Compare commits

..
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
6ba3b30b60 Version Packages (#6030)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-24 15:08:14 +13:00
Joaquim VergesandGitHub 5ad442cf0c [SDK] fix: Simplify in-app wallet logout flow (#6046) 2025-01-24 14:49:15 +13:00
Phillip HoandGitHub 5756c74a71 [service-utils] feat: Add usageV2 support (#6021) 2025-01-24 08:52:35 +08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a1cff7a579 build(deps-dev): bump checkly from 4.15.0 to 4.18.0 (#6045)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-24 13:46:30 +13:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
eafaefec20 build(deps-dev): bump vite from 6.0.10 to 6.0.11 in the npm_and_yarn group (#6022)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-24 13:37:31 +13:00
Manan TankandGitHub b81d7a1854 [TOOL-3207] Portal: Delete V4 SDK pages, Setup knip lint, Fix lint config (#6039) 2025-01-24 13:37:03 +13:00
Winston YeoandGitHub 21cc45adc2 Winston/tool 3161 cannot create backend wallets using latest version (#6025) 2025-01-24 13:36:33 +13:00
Joaquim VergesandGitHub a67d342779 [SDK] fix: Improve smart wallet signature verification (#6040) 2025-01-24 13:35:58 +13:00
Joaquim VergesandGitHub 60edce49c0 [SDK] fix: Improve WalletConnect mobile connection handling (#6042) 2025-01-24 13:35:31 +13:00
Joaquim VergesandGitHub 20c0dca958 [Portal] fix: Update useAutoConnect hook reference link (#6043) 2025-01-24 13:35:11 +13:00
Joaquim VergesandGitHub 4b30b847e2 [Dashboard] fix: Use formatted ABI for contract function calls (#6044) 2025-01-24 13:34:47 +13:00
MananTank b2cc17fd1e [TOOL-3220] Dashboard: Reset Project pagination on filter change, SDK: Fix WalletProvider JSDoc (#6041) 2025-01-23 23:58:47 +00:00
MananTank ace1dc5b09 [TOOL-3124] Dashboard: paginate projects, sort by Montly active users (#6027)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the project analytics in the dashboard by changing how project data is retrieved and displayed. It updates the calculation of user connections to reflect monthly active users and introduces pagination for project listings.

### Detailed summary
- Replaced `getProjectsWithTotalWallets` with `getProjectsWithAnalytics`.
- Updated project data structure to include `monthlyActiveUsers` instead of `totalConnections`.
- Introduced pagination in the `TeamProjectsPage` component.
- Changed sorting criteria from `totalConnections` to `monthlyActiveUsers`.
- Updated UI elements to reflect the new data fields.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-01-23 12:09:59 +00:00
Joaquim VergesandGitHub 9f79277344 [Dashboard] perf: Optimize chain and contract data processing (#6035) 2025-01-23 13:47:14 +13:00
MananTank 6e0fd205ff [TOOL-2964] Dashboard: Fix crash on unix time converter page for invalid unix time, UI improvements (#6036)
=
2025-01-22 23:58:04 +00:00
MananTank 69b1ee9176 [TOOL-3082] Dashboard: Replace NetworkSelectorButton with SingleNetworkSelector where switching chain in wallet is not required (#6031)
<!-- start pr-codex -->

## PR-Codex overview
This PR introduces enhancements to the `SingleNetworkSelector` component and its usage across various files. It adds new props for alignment and positioning, replaces existing network selector buttons, and integrates chain ID handling in several components.

### Detailed summary
- Added `side`, `disableChainId`, and `align` props to `SingleNetworkSelector`.
- Updated `SingleNetworkSelector` to conditionally render the chain ID badge.
- Replaced `NetworkSelectorButton` with `SingleNetworkSelector` in multiple components.
- Integrated `chainId` state management using `useState` in `BackendWalletsSection` and `ImportForm`.
- Updated `BackendWalletsTable` and `BackendWalletBalanceCell` to accept `chainId` as a prop.
- Changed the way active chains are retrieved with `useActiveChainAsDashboardChain` to `useV5DashboardChain`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-01-22 23:48:14 +00:00
MananTank 0a19142a24 [TOOL-3120] Redirect to last visited team page if authenticated (#6028) 2025-01-22 23:39:05 +00:00
MananTank feb4fd0908 [TOOL-3123] Dashboard: Order projects by total wallets (#6026)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the `CopyTextButton` component, optimizing the project fetching logic, and improving project display features by integrating total wallet connections in the dashboard.

### Detailed summary
- Changed `tooltip` type in `CopyTextButton` to `string | undefined`.
- Updated the API call in `getChangelog` to limit results to 7.
- Replaced cookie-based token retrieval with `getAuthToken` in `getProjects`.
- Added `getProjectsWithTotalWallets` to calculate total wallet connections.
- Updated `TeamProjectsPage` to display projects with total connections.
- Modified sorting logic in `TeamProjectsPage` to include total connections.
- Replaced `CopyButton` with `CopyTextButton` in `ProjectCard`.
- Enhanced project display to show total users.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-01-22 23:30:11 +00:00
gregfromstl 1d8695eb42 [SDK] Fix: Opt out of 1559 on lumia mainnet (#6032)
<!-- start pr-codex -->

## PR-Codex overview
This PR adds support for the `Lumia Mainnet` in the fee data configuration.

### Detailed summary
- Added a new entry for `Lumia Mainnet` in the fee data array.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-01-22 22:34:34 +00:00
Joaquim VergesandGitHub 858dc53a25 [Dashboard] fix: Add error reason to analytics API error logs (#6034) 2025-01-23 11:34:16 +13:00
Joaquim VergesandGitHub 8997fac1b0 [Dashboard] fix: Improve error logging for analytics API failures (#6033) 2025-01-23 11:25:35 +13:00
gregfromstl 403e4224e4 Chore: Remove Jakub from CODEOWNERS (#6029)
<!-- start pr-codex -->

## PR-Codex overview
This PR modifies the `.github/CODEOWNERS` file to remove a specific code owner from the `apps/` directory.

### Detailed summary
- Removed `@jakubkrehel` as a code owner for the `apps/` directory.
- Retained `@thirdweb-dev/developer-tools` and `@thirdweb-dev/core-platform` as code owners for the `apps/` directory.
- `scripts/` code ownership remains unchanged with `@jnsdls` and `@joaquim-verges`.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-01-22 20:17:33 +00:00
Phillip HoandGitHub de039ef462 [service-utils] fix: Don't hash cache key (#6023) 2025-01-23 04:16:16 +08:00
Joaquim VergesandGitHub 35fd3bcafa [Dashboard] feat: Add transaction analytics and tracking (#6019) 2025-01-23 08:36:04 +13:00
MananTank 80a22ff45e [TOOL-3173] Portal: Update hero iamge, OG image on landing page (#6017) 2025-01-22 21:46:05 +05:30
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
eefa2ab424 Version Packages (#6018)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-22 17:40:37 +13:00
Joaquim VergesandGitHub ab9a148b75 [SDK] feat: Add transaction tracking for smart wallets (#6020) 2025-01-22 17:12:38 +13:00
gregandGitHub 8bbee03c77 [SDK] Fix: Allow smart wallets for site linking (#6015) 2025-01-21 17:02:11 -08:00
gregandGitHub b38604cce8 [SDK] Fix: Do not remove chain listener on disconnect (#6011) 2025-01-21 15:45:55 -08:00
MananTank 59eda22043 [TOOL-3066] Dashboard: Fix 404 on example asset files, robots.txt, sitemap.xml (#6014) 2025-01-21 21:17:22 +00:00
MananTank 577636c037 [TOOL-3156] Dashboard: Fix localhost engine not loading, fetch engine permission on client side (#6013)
<!-- start pr-codex -->

## PR-Codex overview
This PR enhances the engine access permission handling in the dashboard. It introduces a new component, `EnsureEnginePermission`, which checks permissions before rendering engine details and handles various error states more effectively.

### Detailed summary
- Added `account` to the return object in `getEngineInstancePageMeta.ts`.
- Improved URL handling in `getEngineAccessPermission.ts`.
- Introduced `EnsureEnginePermission` component.
- Replaced direct permission checks in `Layout` with `EnsureEnginePermission`.
- Removed `EngineInstanceLayoutContent` component.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-01-21 20:50:21 +00:00
jnsdls 1d844ac5d6 add connect/pay to framer rewrites (#6012)
closes: CORE-708

### TL;DR
Added `/connect/pay` route to Framer rewrites

### What changed?
Added a new route `/connect/pay` to the list of Framer rewrites in the dashboard application

### How to test?
1. Navigate to `/connect/pay` in the dashboard
2. Verify the route resolves correctly
3. Ensure the page loads without any routing errors

### Why make this change?
To enable proper routing and rendering of the new payment page in the Connect section of the dashboard
2025-01-21 19:21:28 +00:00
MananTank c7be92f75b [TOOL-3166] Update portal theming to match dashboard (#6010)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on updating the styling and structure of various components in the `apps/portal` project, enhancing UI consistency and accessibility. It introduces color changes, layout adjustments, and modifies several components' props for better integration.

### Detailed summary
- Deleted `docs-hero.png` and `docs-hero.svg`.
- Updated `fill` attribute in `UnrealEngineIcon.tsx` and `UnityIcon.tsx` to `currentColor`.
- Changed color definitions in `layout.tsx` and several components to use `text-foreground` and `text-muted-foreground`.
- Modified class names in `Paragraph.tsx`, `InlineCode.tsx`, and `Document` components for improved styling.
- Adjusted button classes in `Button.tsx` and `Feedback.tsx` for consistency.
- Refactored links to use `DocLink` for better routing.
- Updated `Callout` component styling to use `bg-card`.
- Enhanced accessibility features across various components by modifying text colors and ensuring proper contrast.
- Modified `tailwind.config.ts` to introduce new color variables for better theme management.
- Improved layout consistency in multiple `page.mdx` files by adjusting grid gaps and spacing.

> The following files were skipped due to too many changes: `apps/portal/src/app/contracts/deploy/overview/page.mdx`, `apps/portal/src/components/others/ScrollShadow/ScrollShadow.tsx`, `apps/portal/src/components/ui/select.tsx`, `apps/portal/src/components/others/Sidebar.tsx`, `apps/portal/src/app/contracts/explore/pre-built-modular/token-drop/page.mdx`, `apps/portal/src/components/others/DocSearch.tsx`, `apps/portal/src/icons/products/nebula/NebulaIcon.tsx`, `apps/portal/src/app/contracts/explore/pre-built-modular/nft-drop/page.mdx`, `apps/portal/src/app/connect/wallet/overview/page.mdx`, `apps/portal/src/app/globals.css`, `apps/portal/src/app/page.tsx`, `apps/portal/src/app/contracts/explore/pre-built-modular/edition-drop/page.mdx`, `apps/portal/src/icons/sdks/ReactIcon.tsx`, `apps/portal/src/app/contracts/explore/pre-built-modular/edition/page.mdx`, `apps/portal/src/app/contracts/explore/pre-built-modular/nft-collection/page.mdx`, `apps/portal/src/app/Header.tsx`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2025-01-21 19:11:53 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>greg
79ed399e56 build(deps-dev): bump @types/react-dom from 19.0.2 to 19.0.3 (#6000)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: greg <[email protected]>
2025-01-21 21:05:25 +13:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ded5e22bb7 build(deps-dev): bump tailwindcss from 3.4.16 to 3.4.17 (#6001)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-21 21:05:10 +13:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5029345461 build(deps-dev): bump vite from 6.0.3 to 6.0.10 (#6002)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-21 21:04:57 +13:00
550 changed files with 3209 additions and 738883 deletions
+1 -1
View File
@@ -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
+5 -1
View File
@@ -27,5 +27,9 @@
},
"[css]": {
"editor.defaultFormatter": "biomejs.biome"
}
},
"eslint.workingDirectories": [
{ "pattern": "./packages/*/" },
{ "pattern": "./apps/*/" }
]
}
+1
View File
@@ -9,6 +9,7 @@ module.exports = [
// -- connect
"/connect",
"/connect/sign-in",
"/connect/pay",
// -- nebula
"/nebula",
// --insight
+3 -3
View File
@@ -122,14 +122,14 @@
"@types/pluralize": "^0.0.33",
"@types/qrcode": "^1.5.5",
"@types/react": "19.0.2",
"@types/react-dom": "19.0.2",
"@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",
"@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",
@@ -138,7 +138,7 @@
"next-sitemap": "^4.2.3",
"postcss": "8.4.49",
"storybook": "8.4.7",
"tailwindcss": "3.4.16",
"tailwindcss": "3.4.17",
"typescript": "5.7.2"
}
}
@@ -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
1 name description external_url background_color youtube_url additional properties can be added
2 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

+29 -1
View File
@@ -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[]> {
+3 -12
View File
@@ -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;
@@ -90,6 +90,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();
@@ -142,14 +145,17 @@ export function SingleNetworkSelector(props: {
/>
{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;
@@ -168,6 +174,8 @@ export function SingleNetworkSelector(props: {
className={props.className}
popoverContentClassName={props.popoverContentClassName}
disabled={isLoadingChains}
side={props.side}
align={props.align}
/>
);
}
@@ -31,6 +31,8 @@ interface SelectWithSearchProps
) => boolean;
renderOption?: (option: { value: string; label: string }) => React.ReactNode;
popoverContentClassName?: string;
side?: "left" | "right" | "top" | "bottom";
align?: "center" | "start" | "end";
}
export const SelectWithSearch = React.forwardRef<
@@ -107,7 +109,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 +120,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={{
@@ -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?:
@@ -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
@@ -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>
);
}
@@ -1,118 +1,151 @@
"use client";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import {} from "@/components/ui/alert";
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">
&larr; {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>
);
}
+1 -1
View File
@@ -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",
@@ -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"
/>
)}
@@ -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>;
}
@@ -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;
@@ -35,7 +35,10 @@ import { ChainIcon } from "components/icons/ChainIcon";
import { TWTable } from "components/shared/TWTable";
import { useTrack } from "hooks/analytics/useTrack";
import { EngineBackendWalletOptions } from "lib/engine";
import { useActiveChainAsDashboardChain } from "lib/v5-adapter";
import {
useActiveChainAsDashboardChain,
useV5DashboardChain,
} from "lib/v5-adapter";
import {
DownloadIcon,
PencilIcon,
@@ -59,6 +62,7 @@ interface BackendWalletsTableProps {
isPending: boolean;
isFetched: boolean;
authToken: string;
chainId: number;
}
interface BackendWalletDashboard extends BackendWallet {
@@ -72,19 +76,21 @@ 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,
});
const chain = useActiveChainAsDashboardChain();
const chain = useV5DashboardChain(chainId);
if (!chain || !backendWalletBalance) {
return;
}
@@ -100,7 +106,7 @@ const BackendWalletBalanceCell: React.FC<BackendWalletBalanceCellProps> = ({
</Text>
);
const explorer = chain?.explorers?.[0];
const explorer = chain.blockExplorers?.[0];
if (!explorer) {
return balanceComponent;
}
@@ -123,6 +129,7 @@ export const BackendWalletsTable: React.FC<BackendWalletsTableProps> = ({
isPending,
isFetched,
authToken,
chainId,
}) => {
const editDisclosure = useDisclosure();
const receiveDisclosure = useDisclosure();
@@ -163,13 +170,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>();
@@ -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>
);
@@ -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,
};
}
}
@@ -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,6 +157,16 @@ export function TeamProjectsPage(props: {
</div>
)}
{showPagination && (
<div className="py-6">
<PaginationButtons
activePage={page}
onPageClick={setPage}
totalPages={totalPages}
/>
</div>
)}
<LazyCreateAPIKeyDialog
open={isCreateProjectDialogOpen}
onOpenChange={setIsCreateProjectDialogOpen}
@@ -118,7 +183,7 @@ export function TeamProjectsPage(props: {
}
function ProjectCard(props: {
project: Project;
project: ProjectWithAnalytics;
team_slug: string;
}) {
const { project, team_slug } = props;
@@ -130,34 +195,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 +291,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 +306,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) => (
@@ -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}
/>
);
}
@@ -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}
/>
);
}
@@ -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}
@@ -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>
),
)}
@@ -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";
@@ -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[];
@@ -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,
+25
View File
@@ -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()}`;
}
+8 -5
View File
@@ -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()}`;
}
+7
View File
@@ -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;
+2 -2
View File
@@ -50,12 +50,12 @@
"devDependencies": {
"@types/node": "22.10.2",
"@types/react": "19.0.2",
"@types/react-dom": "19.0.2",
"@types/react-dom": "19.0.3",
"eslint": "8.57.0",
"eslint-config-next": "15.1.4",
"eslint-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
"postcss": "8.4.49",
"tailwindcss": "3.4.16",
"tailwindcss": "3.4.17",
"tailwindcss-animate": "^1.0.7",
"typescript": "5.7.2"
}
@@ -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={() => {
@@ -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>
+11 -3
View File
@@ -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"]
}
]
}
+33
View File
@@ -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"
]
}
+8 -7
View File
@@ -10,10 +10,12 @@
"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",
@@ -30,7 +32,6 @@
"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",
@@ -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.3",
"@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/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,13 +66,13 @@
"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",
"knip": "5.41.0",
"next-sitemap": "^4.2.3",
"postcss": "8.4.49",
"tailwindcss": "3.4.16",
"tailwindcss": "3.4.17",
"tsx": "4.19.2",
"typescript": "5.7.2"
}
+21 -41
View File
@@ -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,
};
}
+52 -47
View File
@@ -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

-404
View File
@@ -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;
@@ -3,7 +3,7 @@ import path from "node:path";
import FlexSearch from "flexsearch";
import type { PageData, PageTitleIndex, SectionIndex } from "../types";
export type Indexes = {
type Indexes = {
sectionIndex: SectionIndex;
websiteData: PageData[];
pageTitleIndex: PageTitleIndex;
Binary file not shown.
@@ -28,7 +28,7 @@ export default async function Page(props: {
<Button asChild>
<Link
href="/changelog"
className="-translate-x-1/4 !p-2 !text-f-300 hover:!text-accent-500 mb-4 bg-transparent"
className="-translate-x-1/4 !p-2 !text-muted-foreground hover:!text-foreground mb-4 bg-transparent"
>
<ArrowLeft className="size-6" />
</Link>
@@ -42,7 +42,7 @@ export default async function Page(props: {
<h1
className={cn(
"mb-8 break-words font-bold text-4xl text-f-100 tracking-tight md:text-5xl",
"mb-8 break-words font-bold text-4xl text-foreground tracking-tight md:text-5xl",
)}
>
{data.title}
@@ -3,16 +3,27 @@
}
.changelog-page a {
color: var(--accent-500);
text-decoration: underline;
text-decoration-color: hsl(var(--muted-foreground));
text-decoration-style: dotted;
color: hsl(var(--foreground));
text-underline-offset: 5px;
transition: color 200ms ease;
}
.changelog-page h2 a {
text-decoration: none;
}
.changelog-page a:hover {
color: var(--f-100);
color: hsl(var(--foreground));
text-decoration-color: hsl(var(--foreground));
text-decoration-style: solid;
}
.changelog-page figcaption {
text-align: center;
color: var(--f-300);
color: hsl(var(--muted-foreground));
}
.changelog-page iframe {
@@ -43,7 +54,7 @@
display: flex;
gap: 10px;
margin: 20px 0;
background: var(--b-800);
background: hsl(var(--card));
}
.kg-video-overlay {
@@ -10,7 +10,9 @@ export function Author(props: { name: string; profileImage?: string | null }) {
alt=""
/>
)}
<span className="font-medium text-f-200 text-sm">{props.name}</span>
<span className="font-medium text-muted-foreground text-sm">
{props.name}
</span>
</div>
);
}
@@ -5,7 +5,10 @@ export function RenderDate(props: { iso: string; className?: string }) {
return (
<time
dateTime={props.iso}
className={cn("font-medium text-f-300 text-sm", props.className)}
className={cn(
"font-medium text-muted-foreground text-sm",
props.className,
)}
>
{formatDistance(new Date(props.iso), new Date())} ago
</time>
+1 -1
View File
@@ -1,7 +1,7 @@
import type { PostsOrPages } from "@tryghost/content-api";
import "./[slug]/styles.css";
export const GHOST_THIRDWEB_BLOG_KEY = "49c62b5137df1c17ab6b9e46e3";
const GHOST_THIRDWEB_BLOG_KEY = "49c62b5137df1c17ab6b9e46e3";
export async function fetchChangeLogs() {
const queryParamsString = Object.entries({
+5 -4
View File
@@ -65,20 +65,21 @@ async function PageContent() {
</div>
</div>
<div className="-left-12 -translate-x-1/2 absolute top-12 hidden size-7 items-center justify-center rounded-[50%] bg-f-100 md:size-10 xl:flex">
<PlusIcon className="size-6 text-b-900" />
<div className="-left-12 -translate-x-1/2 absolute top-12 hidden size-7 items-center justify-center rounded-[50%] bg-foreground md:size-10 xl:flex">
<PlusIcon className="size-6 text-background" />
</div>
<div className="overflow-hidden">
<div>
<Heading
level={2}
// eslint-disable-next-line tailwindcss/no-custom-classname
className="changelog-title"
id={slugger.slug(post.title || "")}
anchorClassName="[&>a]:hidden m-0 border-b pb-5 mb-5"
>
<Link
href={`/changelog/${post.slug}`}
className="!text-f-100 font-bold text-3xl tracking-tighter hover:underline md:text-4xl xl:text-5xl"
className="!text-foreground font-bold text-3xl tracking-tighter hover:underline md:text-4xl xl:text-5xl"
>
{post.title}
</Link>
@@ -60,7 +60,7 @@ export const transform: Transform = (node, index: number) => {
if (!node.attribs) {
node.attribs = {};
}
node.attribs.class = "text-center text-sm text-f-300";
node.attribs.class = "text-center text-sm text-muted-foreground";
return;
}
return <Paragraph>{getChildren()}</Paragraph>;
-10
View File
@@ -16,16 +16,6 @@ npx thirdweb install <projectpath> [options]
This command will automatically detect the type of project you are working on and install or update the appropriate thirdweb dependencies.
**Dependencies installed:**
- **JS/TS**
- **Smart contract projects**: [`@thirdweb/contracts`](/typescript/latest)
- **App projects:**
- **React**: [`@thirdweb/react`](/react/latest), [`@thirdweb/sdk`](/typescript/latest) and [`ethers` v5](https://docs.ethers.org/v5/)
- **React Native**: [`@thirdweb/react-native`](/react-native/latest), [`@thirdweb/react-native-compat`](/react-native/latest) and [`ethers` v5](https://docs.ethers.org/v5/)
- **Other**: [`@thirdweb/sdk`](/typescript/latest) and [`ethers` v5](https://docs.ethers.org/v5/)
If any thirdweb package _(or Ethers)_ is already installed, they are updated to the latest compatible version.
## Options
@@ -183,7 +183,7 @@ string address = await sdk.wallet.Connect(connection);
</Step>
<Step title="Executing Transactions with Smart Accounts">
Once setup, you can use the Connect [TypeScript](/typescript/latest), [React](/react/latest) and [Unity SDKs](/unity) to deploy contracts, perform transactions, and manipulate smart accounts like any other wallet.
Once setup, you can use the Connect [TypeScript](/typescript/v5), [React](/react/v5) and [Unity SDKs](/unity) to deploy contracts, perform transactions, and manipulate smart accounts like any other wallet.
<Tabs defaultValue="react">
@@ -14,7 +14,7 @@ export const metadata = createMetadata({
# Using Account Abstraction in React
By using the [wallet SDK](/references/wallets/latest) alongside the [React SDK](/react/latest), you can use smart accounts in your front-end applications easily.
By using the [TypeScript SDK](/typescript/v5), you can use smart accounts in your front-end applications easily.
<Steps>
@@ -14,7 +14,7 @@ export const metadata = createMetadata({
# Using Account abstraction in Typescript
By using the [wallet SDK](/references/wallets/latest) alongside the [TypeScript SDK](/typescript/latest), you can use smart accounts in your applications easily.
By using the [TypeScript SDK](/typescript/v5), you can use smart accounts in your applications easily.
## Example Use Cases
@@ -80,15 +80,18 @@ console.log("Personal account address:", personalAccount.address);
</Step>
<Step title="Creating the Smart account">
Now, let's create a smart account using the SmartWallet class from the `@thirdweb-dev/wallets` package.
To do this, we need to pass a `SmartWalletConfig` object to the constructor. This object contains the following properties:
Now, let's create a smart account using `smartWallet` from `thirdweb/wallets` package.
To do this, we need to pass an object of type [SmartWalletOptions](/references/typescript/v5/SmartWalletOptions) to the function.
Some of the important properties in this object are:
- `chain`: the chain that the smart account will be deployed on.
- `sponsorGas`: whether the smart account should have sponsored transactions or not.
Once we have created the config and instantiated the `SmartWallet` class, we can connect the personal wallet to the smart account using the `connect` method.
Once we have created the smart wallet interface, we can connect the personal wallet to the smart account using the `connect` method.
```typescript
import { smartWallet } from "thirdweb/wallets";
// Configure the smart wallet
const wallet = smartWallet({
chain: sepolia,
@@ -1,6 +1,5 @@
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { createMetadata } from "@doc";
import { V4SDKbanner } from "@/components/others/V4SDKBanner";
export const metadata = createMetadata({
image: {
@@ -11,7 +11,6 @@ import {
TabsTrigger,
} from "@doc";
import { GraduationCap } from "lucide-react";
import { V4SDKbanner } from "@/components/others/V4SDKBanner";
export const metadata = createMetadata({
image: {
@@ -11,7 +11,6 @@ import {
TabsTrigger,
} from "@doc";
import { GraduationCap } from "lucide-react";
import { V4SDKbanner } from "@/components/others/V4SDKBanner";
export const metadata = createMetadata({
image: {
+1 -1
View File
@@ -9,7 +9,7 @@ export default async function Layout(props: { children: React.ReactNode }) {
editPageButton={true}
sidebarHeader={
<div className="flex-col items-center gap-1">
<p className="py-5 font-semibold text-f-100 text-lg">Connect</p>
<p className="py-5 font-semibold text-foreground text-lg">Connect</p>
<PlatformSelector selected="Overview" />
</div>
}
+16 -26
View File
@@ -12,7 +12,13 @@ import {
ArticleIconCard,
} from "@doc";
import { ExternalLink } from "lucide-react";
import { TypeScriptIcon, ReactIcon, DotNetIcon, UnityIcon, UnrealEngineIcon } from "@/icons";
import {
TypeScriptIcon,
ReactIcon,
DotNetIcon,
UnityIcon,
UnrealEngineIcon,
} from "@/icons";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { Button } from "@/components/ui/button";
@@ -55,26 +61,14 @@ Get started with Connect in your preferred language.
href="/typescript/v5/getting-started"
icon={TypeScriptIcon}
/>
<SDKCard
title="React"
href="/react/v5/getting-started"
icon={ReactIcon}
/>
<SDKCard title="React" href="/react/v5/getting-started" icon={ReactIcon} />
<SDKCard
title="React Native"
href="/react-native/v5/getting-started"
icon={ReactIcon}
/>
<SDKCard
title="DotNet"
href="/dotnet/getting-started"
icon={DotNetIcon}
/>
<SDKCard
title="Unity"
href="/unity/v5/getting-started"
icon={UnityIcon}
/>
<SDKCard title="DotNet" href="/dotnet/getting-started" icon={DotNetIcon} />
<SDKCard title="Unity" href="/unity/v5/getting-started" icon={UnityIcon} />
<SDKCard
title="Unreal Engine"
href="/unreal-engine/getting-started"
@@ -96,15 +90,12 @@ Get started with Connect in your preferred language.
<p>
You'll need a client ID to access Connect's free blockchain APIs, storage, and more.
</p>
<Link
href="https://thirdweb.com/create-api-key"
target="_blank"
>
<Button>
Generate
</Button>
</Link>
</div>
<Button asChild variant='outline'>
<Link href="https://thirdweb.com/create-api-key" target="_blank"> Generate </Link>
</Button>
</div>
</Callout>
## Supported Chains
@@ -120,4 +111,3 @@ View all available starter kits, demos and templates on Github.
href="https://github.com/thirdweb-example"
tag="Starter"
/>
@@ -1 +0,0 @@
test
@@ -24,7 +24,7 @@ On the thirdweb dashboard, navigate to settings to create an API Key to obtain
Visit the [Ecosystem Wallets dashboard](https://thirdweb.com/team/~/~/ecosystem/create) and enter the name of your ecosystem and choose your logo.
<Callout variant="info">
<Callout variant="warning" title="Ecosystem name is Immutable">
Your ecosystem name is immutable and will appear wherever your wallet is
available, so choose carefully.
</Callout>
@@ -1,5 +1,11 @@
import { ArticleIconCard, Callout, DocImage, createMetadata, FeatureCard } from "@doc";
import { WalletsConnectIcon } from "@/icons"
import {
ArticleCard,
Callout,
DocImage,
createMetadata,
FeatureCard,
} from "@doc";
import { WalletsConnectIcon } from "@/icons";
import customModal from "./assets/custom-modals.png";
import EcosystemWalletOverview from "./assets/ecosystem-wallet.png";
@@ -33,33 +39,31 @@ A flexible sign-up flow that accommodates different preferences is critical when
description="500+ wallet providers supported"
iconUrl="/icons/feature-cards/wallet.svg"
/>
<FeatureCard
title="Fiat On-Ramp"
description="Enable purchase options directly in modal"
iconUrl="/icons/feature-cards/fiat-ramp.svg"
/>
<FeatureCard
title="Seamless onboarding"
description="Email, phone, social, and passkey options"
iconUrl="/icons/feature-cards/fingerprint.svg"
/>
<FeatureCard
<FeatureCard
title="Fiat On-Ramp"
description="Enable purchase options directly in modal"
iconUrl="/icons/feature-cards/fiat-ramp.svg"
/>
<FeatureCard
title="Seamless onboarding"
description="Email, phone, social, and passkey options"
iconUrl="/icons/feature-cards/fingerprint.svg"
/>
<FeatureCard
title="Customizable"
description="Match your applications branding"
iconUrl="/icons/feature-cards/customize.svg"
/>
<FeatureCard
title="Gasless"
description="Enable free transactions with smart accounts"
description="Enable free transactions with smart accounts"
iconUrl="/icons/feature-cards/gasless.svg"
/>
<FeatureCard
title="Localization"
description="Support for multiple languages"
iconUrl="/icons/feature-cards/localization.svg"
/>
<FeatureCard
title="Localization"
description="Support for multiple languages"
iconUrl="/icons/feature-cards/localization.svg"
/>
<FeatureCard
title="Cross-Chain Support"
description="Enable end users to view assets, balances, and activities for apps and games across any EVM-compatible chain"
@@ -74,11 +78,11 @@ A flexible sign-up flow that accommodates different preferences is critical when
### Solutions
| SOLUTION | BEST FOR | PRICING |
|----------|----------|----------|
| External Wallets | Applications needing traditional EOA wallet integration | Free |
| In-App Wallet | Mainstream applications requiring seamless onboarding | [Free up to 1,000 MAW, then $0.02/MAW](https://thirdweb.com/pricing) |
| Ecosystem Wallet | Organizations building branded wallet networks | [$250/mo](https://thirdweb.com/pricing) |
| SOLUTION | BEST FOR | PRICING |
| ---------------- | ------------------------------------------------------- | -------------------------------------------------------------------- |
| External Wallets | Applications needing traditional EOA wallet integration | Free |
| In-App Wallet | Mainstream applications requiring seamless onboarding | [Free up to 1,000 MAW, then $0.02/MAW](https://thirdweb.com/pricing) |
| Ecosystem Wallet | Organizations building branded wallet networks | [$250/mo](https://thirdweb.com/pricing) |
### Use Cases
@@ -87,8 +91,7 @@ A flexible sign-up flow that accommodates different preferences is critical when
- **DeFi & NFT** - Traditional wallet connection and fiat on-ramps
- **Chain Ecosystems** - Unified accounts across chain applications
<ArticleIconCard
icon={WalletsConnectIcon}
<ArticleCard
title="Getting started"
description="See our get started guide for quickly getting up and running with thirdweb wallets on the various supported platform."
href="/connect/wallet/get-started"
@@ -27,7 +27,7 @@ Sometimes users want to get started using your app without signing in. You can n
In general, logging in with guest mode is the same as configuring any other login method in the platform of your choice. See the [login methods overview](/connect/wallet/sign-in-methods/configure) for more information on configuring your login options.
### In App Wallet
<div className="my-4 grid gap-2 md:grid-cols-2 lg:grid-cols-3">
<div className="my-4 grid gap-3 md:grid-cols-2 lg:grid-cols-3">
<ArticleIconCard
title="TypeScript"
icon={TypeScriptIcon}
@@ -54,11 +54,11 @@ In general, logging in with guest mode is the same as configuring any other logi
icon={UnityIcon}
href="/unity/v5/wallets/in-app-wallet#login-with-guest---onboard-easily-link-other-accounts-later"
/>
</div>
### Ecosystem Wallet
<div className="my-4 grid gap-2 md:grid-cols-2 lg:grid-cols-3">
<div className="my-4 grid gap-3 md:grid-cols-2 lg:grid-cols-3">
<ArticleIconCard
title="TypeScript"
icon={TypeScriptIcon}
@@ -95,7 +95,7 @@ When your user is ready, link any other auth method so they can access their acc
Below are the guides for linking a new identity for in-app wallet on the various platforms:
<div className="my-4 grid gap-2 md:grid-cols-2 lg:grid-cols-3">
<div className="my-4 grid gap-3 md:grid-cols-2 lg:grid-cols-3">
<ArticleIconCard
title="TypeScript"
icon={TypeScriptIcon}
@@ -128,7 +128,7 @@ Below are the guides for linking a new identity for in-app wallet on the various
Below are the guides for linking a new identity for ecosystem wallet on the various platforms:
<div className="my-4 grid gap-2 md:grid-cols-2 lg:grid-cols-3">
<div className="my-4 grid gap-3 md:grid-cols-2 lg:grid-cols-3">
<ArticleIconCard
title="TypeScript"
icon={TypeScriptIcon}
@@ -159,7 +159,7 @@ Below are the guides for linking a new identity for ecosystem wallet on the vari
## Wrapping Up
Your user can now access this same wallet with their Google account.
Your user can now access this same wallet with their Google account.
While it is not linked to any other identity, the lifetime of the guest wallet for any given user will last until they clear their browser storage.
@@ -25,47 +25,46 @@ npx thirdweb deploy
<div
className="my-4 grid gap-2 md:grid-cols-2 lg:grid-cols-2 "
>
<FeatureCard
title="Automatically verified"
description="Contracts are automatically verified on Sourcify"
iconUrl="/icons/feature-cards/verified.svg"
/>
<FeatureCard
title="Deploy to any EVM"
description="Deploy to over 2,000+ available EVM chains"
iconUrl="/icons/feature-cards/evm.svg"
/>
<FeatureCard
title="Increased security"
description="Avoid exporting private keys with client-side deployment"
iconUrl="/icons/feature-cards/security.svg"
/>
<FeatureCard
title="Streamlined deployment"
description="Avoid writing scripts, setting up RPCs, copying ABIs, etc."
iconUrl="/icons/feature-cards/streamlined-deployment.svg"
/>
<FeatureCard
title="Support for frameworks"
description="Works with contracts built with Hardhat and Foundry"
iconUrl="/icons/feature-cards/frameworks.svg"
/>
<FeatureCard
title="Automatically verified"
description="Contracts are automatically verified on Sourcify"
iconUrl="/icons/feature-cards/verified.svg"
/>
<FeatureCard
title="Deploy to any EVM"
description="Deploy to over 2,000+ available EVM chains"
iconUrl="/icons/feature-cards/evm.svg"
/>
<FeatureCard
title="Increased security"
description="Avoid exporting private keys with client-side deployment"
iconUrl="/icons/feature-cards/security.svg"
/>
<FeatureCard
title="Streamlined deployment"
description="Avoid writing scripts, setting up RPCs, copying ABIs, etc."
iconUrl="/icons/feature-cards/streamlined-deployment.svg"
/>
<FeatureCard
title="Support for frameworks"
description="Works with contracts built with Hardhat and Foundry"
iconUrl="/icons/feature-cards/frameworks.svg"
/>
<FeatureCard
title="Deploy with any wallet"
description="Use any supported EOA or in-app wallet to deploy"
iconUrl="/icons/feature-cards/wallet.svg"
/>
<FeatureCard
title="Dashboard"
description="Unlock a management dashboard"
iconUrl="/icons/feature-cards/dashboard.svg"
/>
<FeatureCard
title="Publish options"
description="Include versioning and a landing page for published contracts"
iconUrl="/icons/feature-cards/versioning.svg"
/>
<FeatureCard
title="Deploy with any wallet"
description="Use any supported EOA or in-app wallet to deploy"
iconUrl="/icons/feature-cards/wallet.svg"
/>
<FeatureCard
title="Dashboard"
description="Unlock a management dashboard"
iconUrl="/icons/feature-cards/dashboard.svg"
/>
<FeatureCard
title="Publish options"
description="Include versioning and a landing page for published contracts"
iconUrl="/icons/feature-cards/versioning.svg"
/>
</div>
</div>
@@ -62,7 +62,7 @@ This is useful when you want _other_ wallets to mint NFTs from your drop into th
To lazy-mint NFT(s), click **Create** to lazy-mint a single NFT, or **Batch Upload** to lazy-mint multiple NFTs at once.
To get started, you can use one of our example files for [CSV](https://thirdweb.com/example.csv) or [JSON](https://nightly.thirdweb.com/example.json) metadata.
To get started, you can use one of our example files for [CSV](https://thirdweb.com/assets/examples/example.csv) or [JSON](https://thirdweb.com/assets/examples/example.json) metadata.
### Setting Claim Phases
@@ -89,7 +89,7 @@ To add a claim phase to a token, click **Add Initial Claim Phase**:
Click **Airdrop** on the token you want to use, and upload a `.csv` file containing a list of addresses you want to airdrop NFTs to.
In the `.csv` file, you must specify an `address` column and a `quantity` column, representing the address you want to send the NFTs and how many you wish to send.
You can download our example [CSV File](https://thirdweb.com/airdrop.csv) to get started.
You can download our example [CSV File](https://thirdweb.com/assets/examples/airdrop.csv) to get started.
### Contract Metadata
@@ -47,7 +47,7 @@ Click on the token you want to use, navigate to the **Airdrop** tab,
and upload a `.csv` file containing a list of addresses you want to airdrop NFTs to.
In the `.csv` file, you must specify an `address` column and a `quantity` column, representing the address you want to send the NFTs and how many you wish to send.
You can download our example [CSV File](https://thirdweb.com/airdrop.csv) to get started.
You can download our example [CSV File](https://thirdweb.com/assets/examples/airdrop.csv) to get started.
### Contract Metadata
@@ -7,9 +7,9 @@ import editionDropStep4 from './assets/edition_drop_step4.png'
# Deploy a modular Edition Drop contract
The Edition Drop contract also known as “semi-fungible” or “multi” tokens create multiple NFTs from the same asset based on the [ERC-1155 standard](https://eips.ethereum.org/EIPS/eip-1155).
The Edition Drop contract also known as “semi-fungible” or “multi” tokens create multiple NFTs from the same asset based on the [ERC-1155 standard](https://eips.ethereum.org/EIPS/eip-1155).
Edition Drop includes settings called claim conditions which allow deployers to set a prices for tokens per claim phase and limits on how many tokens to be released.
Edition Drop includes settings called claim conditions which allow deployers to set a prices for tokens per claim phase and limits on how many tokens to be released.
<ContractDeployCard contractName="Modular Edition Drop" href="https://thirdweb.com/thirdweb.eth/ERC1155CoreInitializable?module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IkNsYWltYWJsZUVSQzExNTUifQ%3D%3D&module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IkJhdGNoTWV0YWRhdGFFUkMxMTU1In0%3D&module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IlJveWFsdHlFUkMxMTU1In0%3D&module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IlNlcXVlbnRpYWxUb2tlbklkRVJDMTE1NSJ9&displayName=Modular+Edition+Drop" />
@@ -33,7 +33,7 @@ The Edition Drop contract allows users to claim tokens based on specific setting
### Deploy modular Edition Drop contract
Learn how to deploy a modular Edition Drop contract through the dashboard.
Learn how to deploy a modular Edition Drop contract through the dashboard.
<Steps>
@@ -46,7 +46,7 @@ Navigate to Explore, locate the [Modular Edition Drop contract](https://thirdweb
</Step>
<Step title="Configure Contract Metadata">
Fill in the Contract Metadata including Name, Symbol, Description, and Image.
Fill in the Contract Metadata including Name, Symbol, Description, and Image.
Contract Metadata serves as your unique identifier.
@@ -56,7 +56,7 @@ Contract Metadata serves as your unique identifier.
<Step title="Configure Sequential Token Id">
Input a number you want your tokens to start at for the sequential token id. If you don’t have a preference to change it, you may input 0.
Input a number you want your tokens to start at for the sequential token id. If you don’t have a preference to change it, you may input 0.
<DocImage src={editionDropStep3} alt="Screenshot of Edition Drop configure sequential token id" />
@@ -68,16 +68,16 @@ Select the preferred chain you want to deploy on and select Deploy Now.
<DocImage src={editionDropStep4} alt="Screenshot of Edition Drop select chain" />
<Callout variant="info" title="Any EVM Compatible">
The Edition Drop contract is deployable on any EVM compatible network. [View all available networks.](https://thirdweb.com/chainlist)
The Edition Drop contract is deployable on any EVM compatible network. [View all available networks.](https://thirdweb.com/chainlist)
</Callout>
If you are signed in with an EOA wallet, you will be prompted to sign the transaction. After that your contract is deployed and you can begin configuring the modules.
If you are signed in with an EOA wallet, you will be prompted to sign the transaction. After that your contract is deployed and you can begin configuring the modules.
</Step>
</Steps>
### Modular Contract
This contract is created using the modular contract framework. Modules on this contract are upgradeable. [Learn more about Modular Contracts.](https://portal.thirdweb.com/contracts/modular-contracts/overview)
This contract is created using the modular contract framework. Modules on this contract are upgradeable. [Learn more about Modular Contracts.](/contracts/modular-contracts/overview)
### Configure Modules
@@ -86,5 +86,5 @@ To configure the modules, navigate to the Modules view:
(Coming soon)
<Callout variant='info' title="Install Modules">
To install additional modules on this contract, navigate to the Modules view after deploying your contract.
To install additional modules on this contract, navigate to the Modules view after deploying your contract.
</Callout>
@@ -8,7 +8,7 @@ import editionStep4 from './assets/edition_step4.png'
# Deploy a modular Edition Drop
The Edition contract also known as “semi-fungible” or “multi” tokens create multiple NFTs from the same asset based on the [ERC-1155 standard](https://eips.ethereum.org/EIPS/eip-1155). NFTs are minted to the creator’s wallet as they are added to the collection. NFTs are still transferable, able to be sold on a marketplace, and perform any other actions as defined by ERC-1155.
The Edition contract also known as “semi-fungible” or “multi” tokens create multiple NFTs from the same asset based on the [ERC-1155 standard](https://eips.ethereum.org/EIPS/eip-1155). NFTs are minted to the creator’s wallet as they are added to the collection. NFTs are still transferable, able to be sold on a marketplace, and perform any other actions as defined by ERC-1155.
For advanced use cases, Edition enables signature-minting capabilities.
@@ -16,7 +16,7 @@ For advanced use cases, Edition enables signature-minting capabilities.
### Edition vs Edition Drop
Edition does not have lazy minting capabilities or claim conditions. If you’d prefer to define specific settings such as claim limits, start and end times to claim, and more, please refer to the [Edition Drop contract](https://thirdweb.com/thirdweb.eth/ERC1155CoreInitializable?module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IkNsYWltYWJsZUVSQzExNTUifQ%3D%3D&module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IkJhdGNoTWV0YWRhdGFFUkMxMTU1In0%3D&module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IlJveWFsdHlFUkMxMTU1In0%3D&module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IlNlcXVlbnRpYWxUb2tlbklkRVJDMTE1NSJ9&displayName=Modular+Edition+Drop).
Edition does not have lazy minting capabilities or claim conditions. If you’d prefer to define specific settings such as claim limits, start and end times to claim, and more, please refer to the [Edition Drop contract](https://thirdweb.com/thirdweb.eth/ERC1155CoreInitializable?module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IkNsYWltYWJsZUVSQzExNTUifQ%3D%3D&module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IkJhdGNoTWV0YWRhdGFFUkMxMTU1In0%3D&module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IlJveWFsdHlFUkMxMTU1In0%3D&module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IlNlcXVlbnRpYWxUb2tlbklkRVJDMTE1NSJ9&displayName=Modular+Edition+Drop).
### Use Cases & Examples
@@ -47,7 +47,7 @@ Navigate to Explore, locate the [Modular Edition contract](https://thirdweb.com/
</Step>
<Step title="Configure Contract Metadata">
Fill in the Contract Metadata including Name, Symbol, Description, and Image.
Fill in the Contract Metadata including Name, Symbol, Description, and Image.
Contract Metadata serves as your unique identifier.
@@ -57,7 +57,7 @@ Contract Metadata serves as your unique identifier.
<Step title="Configure Sequential Token Id">
Input a number you want your tokens to start at for the sequential token id. If you don’t have a preference to change it, you may input 0.
Input a number you want your tokens to start at for the sequential token id. If you don’t have a preference to change it, you may input 0.
<DocImage src={editionStep3} alt="Screenshot of Edition configure sequential token id" />
@@ -69,16 +69,16 @@ Select the preferred chain you want to deploy on and select Deploy Now.
<DocImage src={editionStep4} alt="Screenshot of Edition select chain" />
<Callout variant="info" title="Any EVM Compatible">
The Edition contract is deployable on any EVM compatible network. [View all available networks.](https://thirdweb.com/chainlist)
The Edition contract is deployable on any EVM compatible network. [View all available networks.](https://thirdweb.com/chainlist)
</Callout>
If you are signed in with an EOA wallet, you will be prompted to sign the transaction. After that your contract is deployed and you can begin configuring the modules.
If you are signed in with an EOA wallet, you will be prompted to sign the transaction. After that your contract is deployed and you can begin configuring the modules.
</Step>
</Steps>
### Modular Contract
This contract is created using the modular contract framework. Modules on this contract are upgradeable. [Learn more about Modular Contracts.](https://portal.thirdweb.com/contracts/modular-contracts/overview)
This contract is created using the modular contract framework. Modules on this contract are upgradeable. [Learn more about Modular Contracts.](/contracts/modular-contracts/overview)
### Configure Modules
@@ -87,5 +87,5 @@ To configure the modules, navigate to the Modules view:
(Coming soon)
<Callout variant='info' title="Install Modules">
To install additional modules on this contract, navigate to the Modules view after deploying your contract.
To install additional modules on this contract, navigate to the Modules view after deploying your contract.
</Callout>
@@ -9,15 +9,15 @@ import collectionMintable from '../assets/erc721_mintable.png'
# Deploy a modular NFT Collection contract
The NFT Collection contract creates a unique collection of NFTs based on the ERC-721A standard. NFTs are minted to the creator’s wallet as they are added to the collection. NFTs are still transferable, able to be sold on a marketplace, and perform any other actions as defined by ERC-721A.
The NFT Collection contract creates a unique collection of NFTs based on the ERC-721A standard. NFTs are minted to the creator’s wallet as they are added to the collection. NFTs are still transferable, able to be sold on a marketplace, and perform any other actions as defined by ERC-721A.
For advanced use-cases requiring , NFT Collection includes signature-based minting capabilities.
For advanced use-cases requiring , NFT Collection includes signature-based minting capabilities.
<ContractDeployCard contractName="Modular NFT Collection" href="https://thirdweb.com/thirdweb.eth/ERC721CoreInitializable?module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6Ik1pbnRhYmxlRVJDNzIxIn0%3D&module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IkJhdGNoTWV0YWRhdGFFUkM3MjEifQ%3D%3D&module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IlRyYW5zZmVyYWJsZUVSQzcyMSJ9&displayName=Modular+NFT+Collection" />
### NFT Collection vs NFT Drop
NFT Collection contract does not have lazy minting capabilities or claim conditions. If you’d prefer to define specific settings such as claim limits, start and end times to claim, and more, please refer to the [NFT Drop contract](https://thirdweb.com/thirdweb.eth/ERC721CoreInitializable?module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IkNsYWltYWJsZUVSQzcyMSJ9&module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IkJhdGNoTWV0YWRhdGFFUkM3MjEifQ%3D%3D&module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IlJveWFsdHlFUkM3MjEifQ%3D%3D&displayName=Modular+NFT+Drop).
NFT Collection contract does not have lazy minting capabilities or claim conditions. If you’d prefer to define specific settings such as claim limits, start and end times to claim, and more, please refer to the [NFT Drop contract](https://thirdweb.com/thirdweb.eth/ERC721CoreInitializable?module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IkNsYWltYWJsZUVSQzcyMSJ9&module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IkJhdGNoTWV0YWRhdGFFUkM3MjEifQ%3D%3D&module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IlJveWFsdHlFUkM3MjEifQ%3D%3D&displayName=Modular+NFT+Drop).
### Use Cases & Examples
@@ -43,7 +43,7 @@ Navigate to Explore, locate the [Modular NFT Collection contract](https://thirdw
</Step>
<Step title="Configure Contract Metadata">
Fill in the Contract Metadata including Name, Symbol, Description, and Image.
Fill in the Contract Metadata including Name, Symbol, Description, and Image.
Contract Metadata serves as your unique identifier.
@@ -58,16 +58,16 @@ Select the preferred chain you want to deploy on and select Deploy Now.
<DocImage src={collectionStep3} alt="Screenshot of Token select chain screen" />
<Callout variant="info" title="Any EVM Compatible">
The NFT Collection contract is deployable on any EVM compatible network. [View all available networks.](https://thirdweb.com/chainlist)
The NFT Collection contract is deployable on any EVM compatible network. [View all available networks.](https://thirdweb.com/chainlist)
</Callout>
If you are signed in with an EOA wallet, you will be prompted to sign the transaction. After that your contract is deployed and you can begin configuring the modules.
If you are signed in with an EOA wallet, you will be prompted to sign the transaction. After that your contract is deployed and you can begin configuring the modules.
</Step>
</Steps>
### Modular Contract
This contract is created using the modular contract framework. Modules on this contract are upgradeable. [Learn more about Modular Contracts.](https://portal.thirdweb.com/contracts/modular-contracts/overview)
This contract is created using the modular contract framework. Modules on this contract are upgradeable. [Learn more about Modular Contracts.](/contracts/modular-contracts/overview)
### Configure Modules
@@ -77,7 +77,7 @@ To configure the modules, navigate to the Modules view:
<DocImage src={collectionMintable} alt="Screenshot of ERC721 Mintable module"/>
- Configure BatchMetadataERC721 to upload the NFT metadata
- Configure BatchMetadataERC721 to upload the NFT metadata
<DocImage src={collectionBatchMetadata} alt="Screenshot of ERC721 BatchMetadata module"/>
@@ -86,6 +86,6 @@ To configure the modules, navigate to the Modules view:
<DocImage src={collectionTransferable} alt="Screenshot of ERC721 Transferable module"/>
<Callout variant='info' title="Install Modules">
To install additional modules on this contract, navigate to the Modules view after deploying your contract.
To install additional modules on this contract, navigate to the Modules view after deploying your contract.
</Callout>
@@ -13,7 +13,7 @@ The NFT Drop contract creates a unique collection of NFTs based on the [ERC-721A
### NFT Drop vs NFT Collection
NFT Drop has the Claimable module enabling claim conditions. If you’d prefer to not define specific settings such as claim limits, start and end times to claim, and more, please refer to the [NFT Collection contract](https://thirdweb.com/thirdweb.eth/ERC721CoreInitializable?module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6Ik1pbnRhYmxlRVJDNzIxIn0%3D&module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IkJhdGNoTWV0YWRhdGFFUkM3MjEifQ%3D%3D&module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IlRyYW5zZmVyYWJsZUVSQzcyMSJ9&displayName=Modular+NFT+Collection).
NFT Drop has the Claimable module enabling claim conditions. If you’d prefer to not define specific settings such as claim limits, start and end times to claim, and more, please refer to the [NFT Collection contract](https://thirdweb.com/thirdweb.eth/ERC721CoreInitializable?module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6Ik1pbnRhYmxlRVJDNzIxIn0%3D&module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IkJhdGNoTWV0YWRhdGFFUkM3MjEifQ%3D%3D&module=eyJwdWJsaXNoZXIiOiJkZXBsb3llci50aGlyZHdlYi5ldGgiLCJtb2R1bGVJZCI6IlRyYW5zZmVyYWJsZUVSQzcyMSJ9&displayName=Modular+NFT+Collection).
### Use Cases & Examples
@@ -29,7 +29,7 @@ NFT Drop has the Claimable module enabling claim conditions. If you’d prefer t
### Deploy a modular NFT Drop contract
Learn how to deploy a modular NFT Drop contract through the dashboard.
Learn how to deploy a modular NFT Drop contract through the dashboard.
<Steps>
@@ -43,7 +43,7 @@ Navigate to Explore, locate the [Modular NFT Drop contract](https://thirdweb.com
</Step>
<Step title="Configure Contract Metadata">
Fill in the Contract Metadata including Name, Symbol, Description, and Image.
Fill in the Contract Metadata including Name, Symbol, Description, and Image.
Contract Metadata serves as your unique identifier.
@@ -58,16 +58,16 @@ Select the preferred chain you want to deploy on and select Deploy Now.
<DocImage src={nftDropStep3} alt="Screenshot of NFT Drop select chain screen" />
<Callout variant="info" title="Any EVM Compatible">
The NFT Drop contract is deployable on any EVM compatible network. [View all available networks.](https://thirdweb.com/chainlist)
The NFT Drop contract is deployable on any EVM compatible network. [View all available networks.](https://thirdweb.com/chainlist)
</Callout>
If you are signed in with an EOA wallet, you will be prompted to sign the transaction. After that your contract is deployed and you can begin configuring the modules.
If you are signed in with an EOA wallet, you will be prompted to sign the transaction. After that your contract is deployed and you can begin configuring the modules.
</Step>
</Steps>
### Modular Contract
This contract is created using the modular contract framework. Modules on this contract are upgradeable. [Learn more about Modular Contracts.](https://portal.thirdweb.com/contracts/modular-contracts/overview)
This contract is created using the modular contract framework. Modules on this contract are upgradeable. [Learn more about Modular Contracts.](/contracts/modular-contracts/overview)
### Configure Modules
@@ -76,5 +76,5 @@ To configure the modules, navigate to the Modules view:
(Coming soon)
<Callout variant='info' title="Install Modules">
To install additional modules on this contract, navigate to the Modules view after deploying your contract.
To install additional modules on this contract, navigate to the Modules view after deploying your contract.
</Callout>
@@ -9,7 +9,7 @@ import tokenDropClaimable from '../assets/erc20_claimable.png'
# Deploy a modular Token Drop Contract
The Token Drop contract creates digital currencies that are released for a set price and is compliant with the [ERC-20](https://eips.ethereum.org/EIPS/eip-20) standard. This contract defines a common set of rules for tokens including how they can be transferred, how transactions are approved, and how data about token balances is accessed.
The Token Drop contract creates digital currencies that are released for a set price and is compliant with the [ERC-20](https://eips.ethereum.org/EIPS/eip-20) standard. This contract defines a common set of rules for tokens including how they can be transferred, how transactions are approved, and how data about token balances is accessed.
Token Drop includes settings called claim conditions which allow deployers to set a prices for tokens per claim phase and limits on how many tokens to be released. These are enabled through the Claimable module.
@@ -32,7 +32,7 @@ The Token Drop contract allows users to claim tokens based on specific settings
### Deploy modular Token Drop
Learn how to deploy a modular Token Drop contract through the dashboard.
Learn how to deploy a modular Token Drop contract through the dashboard.
<Steps>
@@ -46,7 +46,7 @@ Navigate to Explore, locate the [Modular Token Drop contract](https://thirdweb.c
</Step>
<Step title="Configure Contract Metadata">
Fill in the Contract Metadata including Name, Symbol, Description, and Image.
Fill in the Contract Metadata including Name, Symbol, Description, and Image.
Contract Metadata serves as your unique identifier.
@@ -61,16 +61,16 @@ Select the preferred chain you want to deploy on and select Deploy Now.
<DocImage src={tokenDropStep3} alt="Screenshot of Token Drop select chain screen" />
<Callout variant="info" title="Any EVM Compatible">
The Token Drop contract is deployable on any EVM compatible network. [View all available networks.](https://thirdweb.com/chainlist)
The Token Drop contract is deployable on any EVM compatible network. [View all available networks.](https://thirdweb.com/chainlist)
</Callout>
If you are signed in with an EOA wallet, you will be prompted to sign the transaction. After that your contract is deployed and you can begin configuring the modules.
If you are signed in with an EOA wallet, you will be prompted to sign the transaction. After that your contract is deployed and you can begin configuring the modules.
</Step>
</Steps>
### Modular Contract
This contract is created using the modular contract framework. Modules on this contract are upgradeable. [Learn more about Modular Contracts.](https://portal.thirdweb.com/contracts/modular-contracts/overview)
This contract is created using the modular contract framework. Modules on this contract are upgradeable. [Learn more about Modular Contracts.](/contracts/modular-contracts/overview)
### Configure Modules
@@ -85,5 +85,5 @@ To configure the modules, navigate to the Modules view:
<DocImage src={tokenDropClaimable} alt="Screenshot of ERC20 Claimable module"/>
<Callout variant='info' title="Install Modules">
To install additional modules on this contract, navigate to the Modules view after deploying your contract.
To install additional modules on this contract, navigate to the Modules view after deploying your contract.
</Callout>
@@ -49,7 +49,7 @@ remappings = ['@thirdweb-dev=lib/modular-contracts/']
```
> **Note**
> **Note**
> The `Core` contract is the base contract that needs to be inherited for this contract to be recognized as a core contract.
</Step>
@@ -114,8 +114,8 @@ Implement the `getSupportedCallbackFunctions` function. The Core contract is abs
<Step title="Add a Callback Function">
Introduce the `_beforeIncrement` function to use the `beforeIncrement` callback from a module to achieve this, we'll introduce the interface `BeforeIncrementCallback`
> **Note**
> Callback functions are hook-like functionalities that can be used before or after the main functionality of a core contract.
> **Note**
> Callback functions are hook-like functionalities that can be used before or after the main functionality of a core contract.
> In this example, the `beforeIncrement` callback is executed before the main increment functionality.
```solidity
@@ -10,7 +10,7 @@ export const metadata = createMetadata({
},
});
## Create Module Contract
# Create Module Contract
**Prerequisite:** Read “Create Core Contract” before creating a Module.
@@ -56,7 +56,7 @@ contract CounterModule is Module {
}
```
> **Note**
> **Note**
> The `Module` contract is the base contract that needs to be inherited for this contract to be recognized as a Module Contract, and we need to implement the `getModuleConfig` function to prevent the contract to be marked as abstract.
</Step>
@@ -101,7 +101,7 @@ library CounterStorage {
```
> **Note**
> **Note**
> The library `CounterStorage` uses the ERC-7201: Namespace storage layout to store the data. Learn more about [ERC-7201](https://eips.ethereum.org/EIPS/eip-7201).
</Step>
@@ -214,7 +214,7 @@ library CounterStorage {
```
> **Note**
> **Note**
> Fallback functions are extra functionalities that a core contract can use via the Solidity `fallback` function.
</Step>
@@ -281,7 +281,7 @@ library CounterStorage {
```
> **Note**
> **Note**
> Callback functions are hook-like functionalities that can be used before or after the main functionality of a core contract. In this snippet, the `beforeIncrement` callback is used before the main increment functionality.
</Step>
@@ -25,7 +25,7 @@ export const metadata = createMetadata({
},
});
### How to Deploy a Modular Contract
# Deploy a Modular Contract
This guide will show you how to publish a Modular Contract that increments a counter. If you're interested in learning how modular contracts work, refer to [How it works](https://portal.thirdweb.com/contracts/modular-contracts/how-it-works).
If you haven't read through [Create a Core Contract](https://portal.thirdweb.com/contracts/modular-contracts/get-started/create-core-contact) and [Create a Module Contract](https://portal.thirdweb.com/contracts/modular-contracts/get-started/create-module-contract), please do so before proceeding
@@ -10,10 +10,12 @@ export const metadata = createMetadata({
},
});
# Get Started with Modular Contracts
The Get Started documentation for Modular Contracts guides you through building and deploying an example modular contract that increments a counter. It also shows you how to build an application using this contract.
**Prerequisites:**
This guide assumes you have a basic understanding of Solidity and the thirdweb SDK.
**Prerequisites:**
This guide assumes you have a basic understanding of Solidity and the thirdweb SDK.
If you are new to Solidity, we recommend starting with the [Solidity docs](https://docs.soliditylang.org/en/v0.8.26/) and the [thirdweb SDK docs](https://portal.thirdweb.com/typescript/v5/getting-started).
### Objectives

Some files were not shown because too many files have changed in this diff Show More