Compare commits
64
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad7f78ca0b | ||
|
|
0967a5d53a | ||
|
|
d93765ac54 | ||
|
|
169ae22a86 | ||
|
|
04d2bc72e8 | ||
|
|
1a236fd103 | ||
|
|
4691343c7d | ||
|
|
93f225b8a3 | ||
|
|
e9649a2b53 | ||
|
|
4d39fb6ee3 | ||
|
|
1e092a7bff | ||
|
|
80ee3d6670 | ||
|
|
a7fd4e51a3 | ||
|
|
61583cf6f3 | ||
|
|
cf02af06a4 | ||
|
|
6001f2487e | ||
|
|
3f21445369 | ||
|
|
9e8fd89ec0 | ||
|
|
95a87530e3 | ||
|
|
5faaf1be48 | ||
|
|
4847ada819 | ||
|
|
08eea95f9b | ||
|
|
737155ba21 | ||
|
|
73ae5817f2 | ||
|
|
6a0704a19b | ||
|
|
2ef8f2a371 | ||
|
|
d6373b2c67 | ||
|
|
7eaf86a607 | ||
|
|
5788cbdbad | ||
|
|
d9a39cde33 | ||
|
|
62c8e7c0c4 | ||
|
|
f618c5308c | ||
|
|
04943d730b | ||
|
|
2606da6d87 | ||
|
|
9cf137660a | ||
|
|
58ab8c2ee7 | ||
|
|
447a519ddc | ||
|
|
a9b7633d05 | ||
|
|
38e089a5f3 | ||
|
|
ef2f86cff0 | ||
|
|
8fdef82147 | ||
|
|
cbb15692a7 | ||
|
|
1d23e5cd9c | ||
|
|
a17d893811 | ||
|
|
0dcde077f5 | ||
|
|
05a041fac3 | ||
|
|
3d140eed97 | ||
|
|
a5f005fff0 | ||
|
|
fb70cab6d4 | ||
|
|
cd9ea69c13 | ||
|
|
dc892bd136 | ||
|
|
3d7c009d72 | ||
|
|
6dbed0d4fc | ||
|
|
adee1903d5 | ||
|
|
a1d785b8ed | ||
|
|
ba92cd2cbe | ||
|
|
c3d0ea0059 | ||
|
|
35c5f57179 | ||
|
|
2a1f4be48b | ||
|
|
0121ccd702 | ||
|
|
12bcfc596a | ||
|
|
a4ff738268 | ||
|
|
b49c191355 | ||
|
|
06f0765665 |
@@ -60,7 +60,7 @@ jobs:
|
||||
uses: ./.github/composite-actions/install
|
||||
|
||||
- name: Setup Biome
|
||||
uses: biomejs/setup-biome@c016c38f26f2c4a6eb3662679143614a254263fd # v2.3.0
|
||||
uses: biomejs/setup-biome@f382a98e582959e6aaac8e5f8b17b31749018780 # v2.5.0
|
||||
with:
|
||||
version: latest
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Preview } from "@storybook/react";
|
||||
import "@/styles/globals.css";
|
||||
import "../src/global.css";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import { ThemeProvider, useTheme } from "next-themes";
|
||||
@@ -81,23 +81,18 @@ function StoryLayout(props: {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<div className="flex min-h-dvh min-w-0 flex-col bg-background text-foreground">
|
||||
<div className="flex justify-end gap-2 border-b p-4">
|
||||
<div className="fixed right-0 bottom-0 z-50 flex justify-end gap-2 p-4">
|
||||
<Button
|
||||
onClick={() => setTheme("dark")}
|
||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
size="sm"
|
||||
variant={theme === "dark" ? "default" : "outline"}
|
||||
variant="outline"
|
||||
className="h-auto w-auto shrink-0 rounded-full p-2"
|
||||
>
|
||||
<MoonIcon className="size-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => setTheme("light")}
|
||||
size="sm"
|
||||
variant={theme === "light" ? "default" : "outline"}
|
||||
className="h-auto w-auto shrink-0 rounded-full p-2"
|
||||
>
|
||||
<SunIcon className="size-4" />
|
||||
{theme === "dark" ? (
|
||||
<MoonIcon className="size-4" />
|
||||
) : (
|
||||
<SunIcon className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -49,12 +49,13 @@ module.exports = [
|
||||
"/templates/:template_slug",
|
||||
// -- learn --
|
||||
"/learn",
|
||||
"/learn/tutorials",
|
||||
"/learn/tutorials/:tutorial_slug",
|
||||
"/learn/guides",
|
||||
"/learn/guides/:guide_slug",
|
||||
"/learn/courses",
|
||||
"/learn/glossary",
|
||||
"/learn/glossary/:glossary_slug",
|
||||
// -- faucets --
|
||||
"/faucets",
|
||||
// -- brand kit --
|
||||
"/brand-kit",
|
||||
];
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"ioredis": "^5.6.0",
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"lucide-react": "0.483.0",
|
||||
"next": "15.2.3",
|
||||
"next": "15.2.4",
|
||||
"next-plausible": "^3.12.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
@@ -106,8 +106,8 @@
|
||||
"devDependencies": {
|
||||
"@chakra-ui/cli": "^2.4.1",
|
||||
"@chromatic-com/storybook": "3.2.6",
|
||||
"@next/bundle-analyzer": "15.2.3",
|
||||
"@next/eslint-plugin-next": "15.2.3",
|
||||
"@next/bundle-analyzer": "15.2.4",
|
||||
"@next/eslint-plugin-next": "15.2.4",
|
||||
"@playwright/test": "1.51.1",
|
||||
"@storybook/addon-essentials": "8.6.4",
|
||||
"@storybook/addon-interactions": "8.6.4",
|
||||
@@ -135,7 +135,7 @@
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301",
|
||||
"eslint-plugin-storybook": "0.11.6",
|
||||
"eslint-plugin-storybook": "0.12.0",
|
||||
"knip": "5.46.0",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.5.3",
|
||||
|
||||
@@ -361,6 +361,19 @@ async function redirects() {
|
||||
"/team/:team_slug/:project_slug/connect/universal-bridge/:path*",
|
||||
permanent: false,
|
||||
},
|
||||
|
||||
// all /learn/tutorials (and sub-routes) -> /learn/guides
|
||||
{
|
||||
source: "/learn/tutorials/:path*",
|
||||
destination: "/learn/guides/:path*",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/learn/tutorials",
|
||||
destination: "/learn/guides",
|
||||
permanent: false,
|
||||
},
|
||||
|
||||
...legacyDashboardToTeamRedirects,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -270,9 +270,10 @@ type ActiveStatus = {
|
||||
ecosystemWallet: boolean;
|
||||
};
|
||||
|
||||
export async function isProjectActive(
|
||||
params: AnalyticsQueryParams,
|
||||
): Promise<ActiveStatus> {
|
||||
export async function isProjectActive(params: {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
}): Promise<ActiveStatus> {
|
||||
const searchParams = buildSearchParams(params);
|
||||
const res = await fetchAnalytics(
|
||||
`v2/active-usage?${searchParams.toString()}`,
|
||||
|
||||
@@ -8,13 +8,12 @@ import { GradientAvatar } from "./GradientAvatar";
|
||||
const meta = {
|
||||
title: "blocks/Avatars/GradientAvatar",
|
||||
component: Story,
|
||||
parameters: {},
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
@@ -22,14 +21,14 @@ const client = getThirdwebClient();
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="flex flex-col gap-10 p-10">
|
||||
<p> All images below are set with size-6 className </p>
|
||||
<div className="container flex max-w-6xl flex-col gap-10 py-10">
|
||||
<p> All images below are set with size-20 className </p>
|
||||
|
||||
<BadgeContainer label="No Src, No id - Skeleton">
|
||||
<GradientAvatar
|
||||
id={undefined}
|
||||
src={undefined}
|
||||
className="size-6"
|
||||
className="size-20"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
@@ -38,7 +37,7 @@ function Story() {
|
||||
<GradientAvatar
|
||||
id={"foo"}
|
||||
src={undefined}
|
||||
className="size-6"
|
||||
className="size-20"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
@@ -47,7 +46,7 @@ function Story() {
|
||||
<GradientAvatar
|
||||
id={"foo"}
|
||||
src={""}
|
||||
className="size-6"
|
||||
className="size-20"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
@@ -56,7 +55,7 @@ function Story() {
|
||||
<GradientAvatar
|
||||
id={"bar"}
|
||||
src={""}
|
||||
className="size-6"
|
||||
className="size-20"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
@@ -65,7 +64,7 @@ function Story() {
|
||||
<GradientAvatar
|
||||
src="invalid-src"
|
||||
id={undefined}
|
||||
className="size-6"
|
||||
className="size-20"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
@@ -74,7 +73,7 @@ function Story() {
|
||||
<GradientAvatar
|
||||
src="https://picsum.photos/200/300"
|
||||
id={undefined}
|
||||
className="size-6"
|
||||
className="size-20"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
@@ -83,7 +82,7 @@ function Story() {
|
||||
<GradientAvatar
|
||||
src="ipfs://QmZbeJYEs7kCJHyQxjxU2SJUtjSAr4m87wzJFJUyWomKdj/Smily.svg"
|
||||
id={undefined}
|
||||
className="size-6"
|
||||
className="size-20"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
@@ -123,14 +122,14 @@ function ToggleTest() {
|
||||
<GradientAvatar
|
||||
src={data?.src}
|
||||
id={data?.id}
|
||||
className="size-6"
|
||||
className="size-20"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="invalid Src">
|
||||
<GradientAvatar
|
||||
className="size-6"
|
||||
className="size-20"
|
||||
src={data ? "invalid-src" : undefined}
|
||||
id={undefined}
|
||||
client={client}
|
||||
|
||||
@@ -14,7 +14,7 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ const client = getThirdwebClient();
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="flex flex-col gap-10 p-10">
|
||||
<div className="container flex max-w-6xl flex-col gap-10 py-10">
|
||||
<p> All images below are set with size-6 className </p>
|
||||
|
||||
<BadgeContainer label="No Src - Skeleton">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
import { DangerSettingCard } from "./DangerSettingCard";
|
||||
|
||||
const meta = {
|
||||
@@ -15,17 +15,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-[1000px] flex-col gap-8 py-10">
|
||||
|
||||
@@ -9,13 +9,12 @@ import { Img } from "./Img";
|
||||
const meta = {
|
||||
title: "blocks/Img",
|
||||
component: Story,
|
||||
parameters: {},
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
import { MultiNetworkSelector } from "./NetworkSelectors";
|
||||
|
||||
const meta = {
|
||||
@@ -16,20 +16,13 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-[1000px] flex-col gap-8 lg:p-10">
|
||||
<div className="container flex max-w-6xl flex-col gap-8 py-10">
|
||||
<Variant label="No Chains selected by default" selectedChainIds={[]} />
|
||||
<Variant
|
||||
label="Polygon, Ethereum selected by default"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
import { SettingsCard } from "./SettingsCard";
|
||||
|
||||
const meta = {
|
||||
@@ -15,139 +15,130 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="min-h-dvh bg-background py-6 text-foreground">
|
||||
<div className="container flex max-w-[1100px] flex-col gap-10 lg:p-10">
|
||||
<BadgeContainer label="No Header">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={undefined}
|
||||
errorText={undefined}
|
||||
noPermissionText={undefined}
|
||||
saveButton={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
<div className="container flex max-w-6xl flex-col gap-10 py-10">
|
||||
<BadgeContainer label="No Header">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={undefined}
|
||||
errorText={undefined}
|
||||
noPermissionText={undefined}
|
||||
saveButton={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Header">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
errorText={undefined}
|
||||
noPermissionText={undefined}
|
||||
saveButton={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
<BadgeContainer label="Header">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
errorText={undefined}
|
||||
noPermissionText={undefined}
|
||||
saveButton={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Error">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
errorText="This is error text"
|
||||
noPermissionText={undefined}
|
||||
saveButton={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
<BadgeContainer label="Error">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
errorText="This is error text"
|
||||
noPermissionText={undefined}
|
||||
saveButton={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="No Permission">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
saveButton={{
|
||||
disabled: false,
|
||||
isPending: false,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText="You do not have permission to edit this"
|
||||
errorText={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
<BadgeContainer label="No Permission">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
saveButton={{
|
||||
disabled: false,
|
||||
isPending: false,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText="You do not have permission to edit this"
|
||||
errorText={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Save">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
saveButton={{
|
||||
disabled: false,
|
||||
isPending: false,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
errorText={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
<BadgeContainer label="Save">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
saveButton={{
|
||||
disabled: false,
|
||||
isPending: false,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
errorText={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Save Disabled">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
saveButton={{
|
||||
disabled: true,
|
||||
isPending: false,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
errorText={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
<BadgeContainer label="Save Disabled">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
saveButton={{
|
||||
disabled: true,
|
||||
isPending: false,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
errorText={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Save Loading">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
saveButton={{
|
||||
disabled: false,
|
||||
isPending: true,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
errorText={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
<BadgeContainer label="Save Loading">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
saveButton={{
|
||||
disabled: false,
|
||||
isPending: true,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
errorText={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { mobileViewport } from "../../../stories/utils";
|
||||
import { SidebarLayout } from "./SidebarLayout";
|
||||
|
||||
const meta = {
|
||||
@@ -15,17 +14,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<SidebarLayout
|
||||
|
||||
@@ -17,6 +17,7 @@ import { NavLink } from "../ui/NavLink";
|
||||
import { Separator } from "../ui/separator";
|
||||
import { MobileSidebar, useActiveSidebarLink } from "./MobileSidebar";
|
||||
import { CustomSidebar, type SidebarLink } from "./Sidebar";
|
||||
import { AppFooter } from "./app-footer";
|
||||
|
||||
export function SidebarLayout(props: {
|
||||
sidebarLinks: SidebarLink[];
|
||||
@@ -54,15 +55,17 @@ export function FullWidthSidebarLayout(props: {
|
||||
footerSidebarLinks?: SidebarLink[];
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
footer?: React.ReactNode;
|
||||
}) {
|
||||
const { contentSidebarLinks, children, footerSidebarLinks } = props;
|
||||
return (
|
||||
<div
|
||||
className={cn("flex w-full flex-1 overflow-y-hidden", props.className)}
|
||||
className={cn(
|
||||
"relative flex w-full flex-1 overflow-y-hidden",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{/* left - sidebar */}
|
||||
<Sidebar collapsible="icon" side="left">
|
||||
<Sidebar collapsible="icon" side="left" className="pt-2">
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent>
|
||||
@@ -92,10 +95,10 @@ export function FullWidthSidebarLayout(props: {
|
||||
links={[...contentSidebarLinks, ...(footerSidebarLinks || [])]}
|
||||
/>
|
||||
|
||||
<main className="container z-0 flex min-w-0 max-w-[1280px] grow flex-col pb-20 max-sm:w-full lg:pt-6">
|
||||
<main className="z-0 flex min-w-0 grow flex-col max-sm:w-full">
|
||||
{children}
|
||||
</main>
|
||||
{props.footer}
|
||||
<AppFooter containerClassName="max-w-7xl" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -153,7 +156,7 @@ function MobileSidebarTrigger(props: { links: SidebarLink[] }) {
|
||||
const activeLink = useActiveSidebarLink(props.links);
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex items-center gap-3 border-b px-4 py-4 lg:hidden">
|
||||
<div className="flex items-center gap-3 border-b px-4 py-4 lg:hidden">
|
||||
<SidebarTrigger className="size-4" />
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
{activeLink && <span className="text-sm">{activeLink.label}</span>}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
import { SingleNetworkSelector } from "./NetworkSelectors";
|
||||
|
||||
const meta = {
|
||||
@@ -16,20 +16,13 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-[1000px] flex-col gap-8 lg:p-10">
|
||||
<div className="container flex max-w-6xl flex-col gap-8 py-10">
|
||||
<Variant label="No Chain ID selected by default" chainId={undefined} />
|
||||
<Variant label="Polygon selected by default" chainId={137} />
|
||||
<Variant
|
||||
|
||||
@@ -12,17 +12,23 @@ import Link from "next/link";
|
||||
|
||||
type AppFooterProps = {
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
};
|
||||
|
||||
export function AppFooter(props: AppFooterProps) {
|
||||
return (
|
||||
<footer
|
||||
className={cn(
|
||||
"w-full border-border border-t bg-card py-6 md:py-8",
|
||||
"w-full border-border border-t py-6 md:py-8",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<div className="container flex flex-col gap-4 md:gap-6">
|
||||
<div
|
||||
className={cn(
|
||||
"container flex flex-col gap-4 md:gap-6",
|
||||
props.containerClassName,
|
||||
)}
|
||||
>
|
||||
{/* top row */}
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
|
||||
@@ -32,6 +32,7 @@ type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
|
||||
description?: string;
|
||||
titleClassName?: string;
|
||||
};
|
||||
customHeader?: React.ReactNode;
|
||||
// chart config
|
||||
config: TConfig;
|
||||
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
|
||||
@@ -41,7 +42,9 @@ type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
|
||||
chartClassName?: string;
|
||||
isPending: boolean;
|
||||
toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode;
|
||||
toolTipValueFormatter?: (value: unknown) => React.ReactNode;
|
||||
hideLabel?: boolean;
|
||||
emptyChartState?: React.ReactElement;
|
||||
};
|
||||
|
||||
export function ThirdwebBarChart<TConfig extends ChartConfig>(
|
||||
@@ -65,12 +68,14 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
|
||||
</CardHeader>
|
||||
)}
|
||||
|
||||
{props.customHeader && props.customHeader}
|
||||
|
||||
<CardContent className={cn(!props.header && "pt-6")}>
|
||||
<ChartContainer config={props.config} className={props.chartClassName}>
|
||||
{props.isPending ? (
|
||||
<LoadingChartState />
|
||||
) : props.data.length === 0 ? (
|
||||
<EmptyChartState />
|
||||
<EmptyChartState>{props.emptyChartState}</EmptyChartState>
|
||||
) : (
|
||||
<BarChart accessibilityLayer data={props.data}>
|
||||
<CartesianGrid vertical={false} />
|
||||
@@ -88,6 +93,7 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
|
||||
props.hideLabel !== undefined ? props.hideLabel : true
|
||||
}
|
||||
labelFormatter={props.toolTipLabelFormatter}
|
||||
valueFormatter={props.toolTipValueFormatter}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -96,25 +102,15 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
|
||||
content={<ChartLegendContent className="pt-5" />}
|
||||
/>
|
||||
)}
|
||||
{configKeys.map((key, idx) => (
|
||||
{configKeys.map((key) => (
|
||||
<Bar
|
||||
key={key}
|
||||
dataKey={key}
|
||||
// if stacked then they should all be the same stackId
|
||||
// if grouped then they should all be unique stackId (so the key works great)
|
||||
stackId={variant === "stacked" ? "a" : key}
|
||||
fill={`var(--color-${key})`}
|
||||
// if stacked then we need to figure out the radius based on the index in the array
|
||||
// if grouped then we can just use the same radius for all
|
||||
radius={
|
||||
variant === "stacked"
|
||||
? idx === 0
|
||||
? [0, 0, 4, 4]
|
||||
: idx === configKeys.length - 1
|
||||
? [4, 4, 0, 0]
|
||||
: [0, 0, 0, 0]
|
||||
: [4, 4, 4, 4]
|
||||
}
|
||||
fill={props.config[key]?.color}
|
||||
radius={[4, 4, 4, 4]}
|
||||
strokeWidth={1}
|
||||
className="stroke-background"
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
import { type CodeEnvironment, CodeSegment } from "./code-segment.client";
|
||||
|
||||
const meta = {
|
||||
@@ -24,17 +24,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
type Mode = "default" | "no-tabs" | "only-tabs";
|
||||
|
||||
function Story() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
import { MultiSelect } from "./multi-select";
|
||||
|
||||
const meta = {
|
||||
@@ -16,17 +16,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function createList(len: number) {
|
||||
return Array.from({ length: len }, (_, i) => ({
|
||||
value: `${i}`,
|
||||
@@ -36,7 +29,7 @@ function createList(len: number) {
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-[600px] flex-col gap-6 px-4 py-6">
|
||||
<div className="container flex max-w-6xl flex-col gap-6 py-10">
|
||||
<VariantTest defaultValues={[]} storyLabel="5 items" listLen={5} />
|
||||
<VariantTest defaultValues={[]} storyLabel="5000 items" listLen={5000} />
|
||||
<VariantTest
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
import { SelectWithSearch } from "./select-with-search";
|
||||
|
||||
const meta = {
|
||||
@@ -16,17 +16,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function createList(len: number) {
|
||||
return Array.from({ length: len }, (_, i) => ({
|
||||
value: `${i}`,
|
||||
@@ -36,7 +29,7 @@ function createList(len: number) {
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-[600px] flex-col gap-6 px-4 py-6">
|
||||
<div className="container flex max-w-6xl flex-col gap-6 py-10">
|
||||
<VariantTest storyLabel="5 items" listLen={5} />
|
||||
<VariantTest storyLabel="5000 items" listLen={5000} />
|
||||
<VariantTest
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer, mobileViewport } from "../../stories/utils";
|
||||
import { BadgeContainer } from "../../stories/utils";
|
||||
import { PaginationButtons } from "./pagination-buttons";
|
||||
|
||||
const meta = {
|
||||
@@ -16,17 +16,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-[1000px] flex-col gap-8 py-10">
|
||||
|
||||
@@ -96,7 +96,10 @@ const BreadcrumbEllipsis = ({
|
||||
<span
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
className={cn(
|
||||
"relative flex h-9 w-9 items-center justify-center",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
|
||||
@@ -6,9 +6,6 @@ import { BadgeContainer } from "../../../stories/utils";
|
||||
const meta = {
|
||||
title: "Shadcn/Buttons",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
} satisfies Meta<typeof Component>;
|
||||
|
||||
export default meta;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { BadgeContainer, mobileViewport } from "stories/utils";
|
||||
import { BadgeContainer } from "stories/utils";
|
||||
import { CodeClient } from "./code.client";
|
||||
|
||||
const meta = {
|
||||
@@ -11,17 +11,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
const tsCode = `\
|
||||
type User = {
|
||||
name: string;
|
||||
@@ -86,7 +79,7 @@ const bashCode = "pnpm i thirdweb";
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div className="container flex max-w-[600px] flex-col gap-10 py-10">
|
||||
<div className="container flex max-w-6xl flex-col gap-10 py-10">
|
||||
<BadgeContainer label="ts">
|
||||
<CodeClient code={tsCode} lang="ts" />
|
||||
</BadgeContainer>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { BadgeContainer, mobileViewport } from "stories/utils";
|
||||
import { BadgeContainer } from "stories/utils";
|
||||
import { PlainTextCodeBlock } from "./plaintext-code";
|
||||
|
||||
const meta = {
|
||||
@@ -11,17 +11,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
const jsCode = `\
|
||||
import { getContract } from "thirdweb";
|
||||
import { sepolia } from "thirdweb/chains";
|
||||
@@ -39,7 +32,7 @@ const overflowText =
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div className="container flex max-w-[600px] flex-col gap-10 py-10">
|
||||
<div className="container flex max-w-6xl flex-col gap-10 py-10">
|
||||
<BadgeContainer label="large chunk of code">
|
||||
<PlainTextCodeBlock code={jsCode} />
|
||||
</BadgeContainer>
|
||||
|
||||
@@ -94,7 +94,10 @@ const PaginationEllipsis = ({
|
||||
}: React.ComponentProps<"span">) => (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
className={cn(
|
||||
"relative flex h-9 w-9 items-center justify-center",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
|
||||
@@ -13,9 +13,6 @@ import { BadgeContainer } from "../../../stories/utils";
|
||||
const meta = {
|
||||
title: "Shadcn/Select",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
} satisfies Meta<typeof Component>;
|
||||
|
||||
export default meta;
|
||||
|
||||
@@ -10,35 +10,25 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import Link from "next/link";
|
||||
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Badge } from "./badge";
|
||||
|
||||
const meta = {
|
||||
title: "Shadcn/Table",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
} satisfies Meta<typeof Component>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div className="flex min-w-0 flex-col gap-6 px-4 py-6 lg:mx-auto lg:max-w-[1000px]">
|
||||
<div className="container flex max-w-6xl flex-col gap-10 py-10">
|
||||
<BadgeContainer label="Normal">
|
||||
<TableDemo />
|
||||
</BadgeContainer>
|
||||
|
||||
@@ -20,6 +20,7 @@ export function TabLinks(props: {
|
||||
className?: string;
|
||||
tabContainerClassName?: string;
|
||||
shadowColor?: string;
|
||||
scrollableClassName?: string;
|
||||
}) {
|
||||
const { containerRef, lineRef, activeTabRef } =
|
||||
useUnderline<HTMLAnchorElement>();
|
||||
@@ -30,7 +31,7 @@ export function TabLinks(props: {
|
||||
<div className="absolute right-0 bottom-0 left-0 h-[1px] bg-border" />
|
||||
|
||||
<ScrollShadow
|
||||
scrollableClassName="pb-[8px] relative"
|
||||
scrollableClassName={cn("pb-[8px] relative", props.scrollableClassName)}
|
||||
shadowColor={props.shadowColor}
|
||||
>
|
||||
<div
|
||||
@@ -159,9 +160,11 @@ function useUnderline<El extends HTMLElement>() {
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const lineEl = lineRef.current;
|
||||
const rect = activeTabEl.getBoundingClientRect();
|
||||
const containerPaddingLeft =
|
||||
containerRect.left - containerRef.current.offsetLeft;
|
||||
lineEl.style.width = `${rect.width}px`;
|
||||
lineEl.style.transform = `translateX(${
|
||||
rect.left - containerRect.left
|
||||
rect.left - containerPaddingLeft
|
||||
}px)`;
|
||||
setTimeout(() => {
|
||||
lineEl.style.transition = "transform 0.3s, width 0.3s";
|
||||
@@ -205,6 +208,7 @@ export function TabPathLinks(props: {
|
||||
className?: string;
|
||||
tabContainerClassName?: string;
|
||||
shadowColor?: string;
|
||||
scrollableClassName?: string;
|
||||
}) {
|
||||
const pathname = usePathname() || "";
|
||||
const { links, ...restProps } = props;
|
||||
|
||||
@@ -47,7 +47,7 @@ export const SearchInput: React.FC = () => {
|
||||
<div className="group relative w-full">
|
||||
<SearchIcon className="-translate-y-1/2 absolute top-[50%] left-3 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search by token address or chain ID"
|
||||
placeholder="Search with anything!"
|
||||
className="h-10 rounded-lg bg-card py-2 pl-9 lg:min-w-[300px]"
|
||||
defaultValue={searchParams?.get("query") || ""}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
|
||||
+72
-8
@@ -7,6 +7,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import type { Address } from "thirdweb";
|
||||
import { checksumAddress } from "thirdweb/utils";
|
||||
import { getRoutes } from "../../../utils";
|
||||
import { ChainlistPagination } from "../client/pagination";
|
||||
import { RouteListCard } from "../server/routelist-card";
|
||||
@@ -34,28 +35,83 @@ async function getRoutesToRender(params: SearchParams) {
|
||||
originTokenAddress?: Address;
|
||||
destinationChainId?: number;
|
||||
destinationTokenAddress?: Address;
|
||||
originTextQuery?: string;
|
||||
destinationTextQuery?: string;
|
||||
}> = {};
|
||||
|
||||
if (params.type === "origin" || typeof params.type === "undefined") {
|
||||
if (params.query?.startsWith("0x")) {
|
||||
filters.originTokenAddress = params.query as Address;
|
||||
} else if (params.query) {
|
||||
} else if (Number.isInteger(Number(params.query))) {
|
||||
filters.originChainId = Number(params.query);
|
||||
} else if (params.query) {
|
||||
filters.originTextQuery = params.query;
|
||||
}
|
||||
} else if (params.type === "destination") {
|
||||
if (params.query?.startsWith("0x")) {
|
||||
filters.destinationTokenAddress = params.query as Address;
|
||||
} else if (params.query) {
|
||||
} else if (Number.isInteger(Number(params.query))) {
|
||||
filters.destinationChainId = Number(params.query);
|
||||
} else if (params.query) {
|
||||
filters.destinationTextQuery = params.query;
|
||||
}
|
||||
}
|
||||
// Temporary, will update this after the /routes endpoint
|
||||
filters.limit = 50_000;
|
||||
|
||||
const routes = await getRoutes(filters);
|
||||
let routes = await getRoutes({ limit: 100_000 });
|
||||
|
||||
const totalCount = routes.length;
|
||||
|
||||
if (filters.originChainId) {
|
||||
routes = routes.filter(
|
||||
(route) => route.originToken.chainId === filters.originChainId,
|
||||
);
|
||||
}
|
||||
if (filters.originTokenAddress) {
|
||||
const originTokenAddress = filters.originTokenAddress;
|
||||
routes = routes.filter(
|
||||
(route) =>
|
||||
checksumAddress(route.originToken.address) ===
|
||||
checksumAddress(originTokenAddress),
|
||||
);
|
||||
}
|
||||
if (filters.destinationChainId) {
|
||||
routes = routes.filter(
|
||||
(route) => route.destinationToken.chainId === filters.destinationChainId,
|
||||
);
|
||||
}
|
||||
if (filters.destinationTokenAddress) {
|
||||
const destinationTokenAddress = filters.destinationTokenAddress;
|
||||
routes = routes.filter(
|
||||
(route) =>
|
||||
checksumAddress(route.destinationToken.address) ===
|
||||
checksumAddress(destinationTokenAddress),
|
||||
);
|
||||
}
|
||||
|
||||
if (filters.originTextQuery) {
|
||||
const originTextQuery = filters.originTextQuery.toLowerCase();
|
||||
routes = routes.filter((route) => {
|
||||
return (
|
||||
route.originToken.name.toLowerCase().includes(originTextQuery) ||
|
||||
route.originToken.symbol.toLowerCase().includes(originTextQuery)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.destinationTextQuery) {
|
||||
const destinationTextQuery = filters.destinationTextQuery.toLowerCase();
|
||||
routes = routes.filter((route) => {
|
||||
return (
|
||||
route.destinationToken.name
|
||||
.toLowerCase()
|
||||
.includes(destinationTextQuery) ||
|
||||
route.destinationToken.symbol
|
||||
.toLowerCase()
|
||||
.includes(destinationTextQuery)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
routesToRender: routes,
|
||||
totalCount,
|
||||
@@ -154,13 +210,21 @@ export async function RoutesData(props: {
|
||||
out of{" "}
|
||||
{filteredCount !== totalCount ? (
|
||||
<>
|
||||
<span className="text-accent-foreground">{filteredCount}</span>{" "}
|
||||
<span className="text-accent-foreground">
|
||||
{filteredCount.toLocaleString()}
|
||||
</span>{" "}
|
||||
routes that match filters. (Total:{" "}
|
||||
<span className="text-accent-foreground">{totalCount}</span>)
|
||||
<span className="text-accent-foreground">
|
||||
{totalCount.toLocaleString()}
|
||||
</span>
|
||||
)
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-accent-foreground">{totalCount}</span> routes.
|
||||
<span className="text-accent-foreground">
|
||||
{totalCount.toLocaleString()}
|
||||
</span>{" "}
|
||||
routes.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
@@ -2,43 +2,18 @@ import "server-only";
|
||||
|
||||
import { BRIDGE_URL, DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/env";
|
||||
import { getAuthToken } from "app/api/lib/getAuthToken";
|
||||
import type { Address } from "thirdweb";
|
||||
import type { Route } from "./types/route";
|
||||
|
||||
export async function getRoutes({
|
||||
limit,
|
||||
offset,
|
||||
originChainId,
|
||||
originTokenAddress,
|
||||
destinationChainId,
|
||||
destinationTokenAddress,
|
||||
}: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
originChainId?: number;
|
||||
originTokenAddress?: Address;
|
||||
destinationChainId?: number;
|
||||
destinationTokenAddress?: Address;
|
||||
} = {}) {
|
||||
const url = new URL(`${BRIDGE_URL}/v1/routes`);
|
||||
if (limit) {
|
||||
url.searchParams.set("limit", limit.toString());
|
||||
}
|
||||
if (offset) {
|
||||
url.searchParams.set("offset", offset.toString());
|
||||
}
|
||||
if (originChainId) {
|
||||
url.searchParams.set("originChainId", originChainId.toString());
|
||||
}
|
||||
if (originTokenAddress) {
|
||||
url.searchParams.set("originTokenAddress", originTokenAddress);
|
||||
}
|
||||
if (destinationChainId) {
|
||||
url.searchParams.set("destinationChainId", destinationChainId.toString());
|
||||
}
|
||||
if (destinationTokenAddress) {
|
||||
url.searchParams.set("destinationTokenAddress", destinationTokenAddress);
|
||||
}
|
||||
// It's faster to filter client side, doesn't seem to be a big performance boost to paginate/filter server side
|
||||
const token = await getAuthToken();
|
||||
const routesResponse = await fetch(url, {
|
||||
headers: token
|
||||
|
||||
+32
-46
@@ -3,14 +3,11 @@ import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { Toaster, toast } from "sonner";
|
||||
import { BadgeContainer, mobileViewport } from "stories/utils";
|
||||
import { toast } from "sonner";
|
||||
import { BadgeContainer } from "stories/utils";
|
||||
import { ZERO_ADDRESS } from "thirdweb";
|
||||
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
|
||||
import {
|
||||
ErrorProvider,
|
||||
type TransactionError,
|
||||
} from "../../../../../../../contexts/error-handler";
|
||||
import type { TransactionError } from "../../../../../../../contexts/error-handler";
|
||||
import { accountStub } from "../../../../../../../stories/stubs";
|
||||
import {
|
||||
BatchMetadataModuleUI,
|
||||
@@ -21,7 +18,6 @@ const meta = {
|
||||
title: "Modules/BatchMetadata",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
@@ -31,17 +27,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Component() {
|
||||
const [isOwner, setIsOwner] = useState(true);
|
||||
|
||||
@@ -80,38 +69,35 @@ function Component() {
|
||||
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<ErrorProvider>
|
||||
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
|
||||
<div>
|
||||
<ConnectButton client={getThirdwebClient()} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-5">
|
||||
<CheckboxWithLabel
|
||||
value={isOwner}
|
||||
onChange={setIsOwner}
|
||||
id="isOwner"
|
||||
label="Is Owner"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<BadgeContainer label="Default">
|
||||
<BatchMetadataModuleUI
|
||||
twAccount={accountStub()}
|
||||
contractInfo={contractInfo}
|
||||
moduleAddress="0x0000000000000000000000000000000000000000"
|
||||
uploadMetadata={uploadMetadataStub}
|
||||
uninstallButton={{
|
||||
onClick: async () => removeMutation.mutateAsync(),
|
||||
isPending: removeMutation.isPending,
|
||||
}}
|
||||
isOwnerAccount={isOwner}
|
||||
contractChainId={1}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
<Toaster richColors />
|
||||
<div className="container flex max-w-6xl flex-col gap-10 py-10">
|
||||
<div>
|
||||
<ConnectButton client={getThirdwebClient()} />
|
||||
</div>
|
||||
</ErrorProvider>
|
||||
|
||||
<div className="flex items-center gap-5">
|
||||
<CheckboxWithLabel
|
||||
value={isOwner}
|
||||
onChange={setIsOwner}
|
||||
id="isOwner"
|
||||
label="Is Owner"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<BadgeContainer label="Default">
|
||||
<BatchMetadataModuleUI
|
||||
twAccount={accountStub()}
|
||||
contractInfo={contractInfo}
|
||||
moduleAddress="0x0000000000000000000000000000000000000000"
|
||||
uploadMetadata={uploadMetadataStub}
|
||||
uninstallButton={{
|
||||
onClick: async () => removeMutation.mutateAsync(),
|
||||
isPending: removeMutation.isPending,
|
||||
}}
|
||||
isOwnerAccount={isOwner}
|
||||
contractChainId={1}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
|
||||
+4
-15
@@ -11,8 +11,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { subDays } from "date-fns";
|
||||
import { useState } from "react";
|
||||
import { Toaster, toast } from "sonner";
|
||||
import { mobileViewport } from "stories/utils";
|
||||
import { toast } from "sonner";
|
||||
import { NATIVE_TOKEN_ADDRESS, ZERO_ADDRESS } from "thirdweb";
|
||||
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
|
||||
import { checksumAddress } from "thirdweb/utils";
|
||||
@@ -29,7 +28,6 @@ const meta = {
|
||||
title: "Modules/Claimable",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
@@ -39,17 +37,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
const testAddress1 = "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37";
|
||||
|
||||
const claimCondition = {
|
||||
@@ -67,7 +58,7 @@ const claimCondition = {
|
||||
|
||||
function Component() {
|
||||
const [isOwner, setIsOwner] = useState(true);
|
||||
const [name, setName] = useState("ClaimableERC721");
|
||||
const [name, setName] = useState("MintableERC721");
|
||||
const [isClaimConditionLoading, setIsClaimConditionLoading] = useState(false);
|
||||
const [isPrimarySaleRecipientLoading, setIsPrimarySaleRecipientLoading] =
|
||||
useState(false);
|
||||
@@ -110,7 +101,7 @@ function Component() {
|
||||
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
|
||||
<div className="container flex max-w-6xl flex-col gap-10 py-10">
|
||||
<div>
|
||||
<ConnectButton client={getThirdwebClient()} />
|
||||
</div>
|
||||
@@ -196,8 +187,6 @@ function Component() {
|
||||
isValidTokenId={true}
|
||||
noClaimConditionSet={noClaimConditionSet}
|
||||
/>
|
||||
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
|
||||
+4
-14
@@ -11,8 +11,8 @@ import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { Toaster, toast } from "sonner";
|
||||
import { BadgeContainer, mobileViewport } from "stories/utils";
|
||||
import { toast } from "sonner";
|
||||
import { BadgeContainer } from "stories/utils";
|
||||
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
|
||||
import { accountStub } from "../../../../../../../stories/stubs";
|
||||
import {
|
||||
@@ -25,7 +25,6 @@ const meta = {
|
||||
title: "Modules/Mintable",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
@@ -35,17 +34,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
const testAddress1 = "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37";
|
||||
|
||||
function Component() {
|
||||
@@ -84,7 +76,7 @@ function Component() {
|
||||
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
|
||||
<div className="container flex max-w-6xl flex-col gap-10 py-10">
|
||||
<div>
|
||||
<ConnectButton client={getThirdwebClient()} />
|
||||
</div>
|
||||
@@ -175,8 +167,6 @@ function Component() {
|
||||
contractChainId={1}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
|
||||
+2
-15
@@ -2,33 +2,22 @@ import { Checkbox } from "@/components/ui/checkbox";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
import { BadgeContainer, mobileViewport } from "stories/utils";
|
||||
import { BadgeContainer } from "stories/utils";
|
||||
import { ThirdwebProvider } from "thirdweb/react";
|
||||
import { ModuleCardUI } from "./module-card";
|
||||
|
||||
const meta = {
|
||||
title: "Modules/ModuleCard",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
} satisfies Meta<typeof Component>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Component() {
|
||||
const [isOwner, setIsOwner] = useState(true);
|
||||
|
||||
@@ -120,8 +109,6 @@ function Component() {
|
||||
</div>
|
||||
</ModuleCardUI>
|
||||
</BadgeContainer>
|
||||
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
|
||||
+3
-13
@@ -3,8 +3,8 @@ import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { Toaster, toast } from "sonner";
|
||||
import { BadgeContainer, mobileViewport } from "stories/utils";
|
||||
import { toast } from "sonner";
|
||||
import { BadgeContainer } from "stories/utils";
|
||||
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
|
||||
import { accountStub } from "../../../../../../../stories/stubs";
|
||||
import {
|
||||
@@ -16,7 +16,6 @@ const meta = {
|
||||
title: "Modules/OpenEditionMetadata",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
@@ -26,17 +25,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Component() {
|
||||
const [isOwner, setIsOwner] = useState(true);
|
||||
async function setSharedMetadataStub(values: SetSharedMetadataFormValues) {
|
||||
@@ -98,8 +90,6 @@ function Component() {
|
||||
twAccount={accountStub()}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
|
||||
+4
-14
@@ -3,8 +3,8 @@ import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { Toaster, toast } from "sonner";
|
||||
import { BadgeContainer, mobileViewport } from "stories/utils";
|
||||
import { toast } from "sonner";
|
||||
import { BadgeContainer } from "stories/utils";
|
||||
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
|
||||
import { accountStub } from "../../../../../../../stories/stubs";
|
||||
import {
|
||||
@@ -18,7 +18,6 @@ const meta = {
|
||||
title: "Modules/Royalty",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
@@ -28,17 +27,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
const _testAddress1 = "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37";
|
||||
|
||||
function Component() {
|
||||
@@ -79,7 +71,7 @@ function Component() {
|
||||
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
|
||||
<div className="container flex max-w-6xl flex-col gap-10 py-10">
|
||||
<div>
|
||||
<ConnectButton client={getThirdwebClient()} />
|
||||
</div>
|
||||
@@ -168,8 +160,6 @@ function Component() {
|
||||
twAccount={twAccount}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
|
||||
+4
-14
@@ -3,8 +3,8 @@ import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { Toaster, toast } from "sonner";
|
||||
import { BadgeContainer, mobileViewport } from "stories/utils";
|
||||
import { toast } from "sonner";
|
||||
import { BadgeContainer } from "stories/utils";
|
||||
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
|
||||
import { accountStub } from "../../../../../../../stories/stubs";
|
||||
import {
|
||||
@@ -16,7 +16,6 @@ const meta = {
|
||||
title: "Modules/Transferable",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
@@ -26,17 +25,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
const testAddress1 = "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37";
|
||||
|
||||
function Component() {
|
||||
@@ -65,7 +57,7 @@ function Component() {
|
||||
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
|
||||
<div className="container flex max-w-6xl flex-col gap-10 py-10">
|
||||
<div className="flex gap-2">
|
||||
<Checkbox
|
||||
id="terms1"
|
||||
@@ -139,8 +131,6 @@ function Component() {
|
||||
twAccount={accountStub()}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ export const products = [
|
||||
id: "contracts",
|
||||
icon: ContractIcon,
|
||||
description: "Pre-built contracts & deployment tools",
|
||||
link: "https://thirdweb.com/contracts",
|
||||
link: "https://thirdweb.com/explore",
|
||||
},
|
||||
{
|
||||
name: "Connect SDK",
|
||||
|
||||
@@ -13,7 +13,7 @@ export function DeployedContractsPageHeader(props: {
|
||||
const [importModalOpen, setImportModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="border-b">
|
||||
<ImportModal
|
||||
isOpen={importModalOpen}
|
||||
onClose={() => {
|
||||
@@ -23,9 +23,11 @@ export function DeployedContractsPageHeader(props: {
|
||||
projectId={props.projectId}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-3 pb-6 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="container flex max-w-7xl flex-col gap-3 py-10 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h1 className="font-semibold text-3xl tracking-tight">Contracts</h1>
|
||||
<h1 className="font-semibold text-2xl tracking-tight lg:text-3xl">
|
||||
Contracts
|
||||
</h1>
|
||||
</div>
|
||||
<div className="flex gap-3 [&>*]:grow">
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowUpRightIcon, DownloadIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { ImportModal } from "../../../../components/contract-components/import-contract/modal";
|
||||
|
||||
export function DeployViaCLIOrImportCard(props: {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
}) {
|
||||
const [importModalOpen, setImportModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-4 lg:p-6">
|
||||
<ImportModal
|
||||
isOpen={importModalOpen}
|
||||
onClose={() => {
|
||||
setImportModalOpen(false);
|
||||
}}
|
||||
teamId={props.teamId}
|
||||
projectId={props.projectId}
|
||||
/>
|
||||
|
||||
<h2 className="mb-0.5 font-semibold text-lg">
|
||||
Already have a smart contract?
|
||||
</h2>
|
||||
<p className="max-w-2xl text-muted-foreground text-sm lg:text-base">
|
||||
Import an already deployed contract or deploy a contract from source
|
||||
code to easily manage permissions, upload assets, and interact with
|
||||
contract functions
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex flex-col gap-3 lg:flex-row">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2 bg-background lg:px-10"
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/contracts/deploy/overview"
|
||||
target="_blank"
|
||||
>
|
||||
Deploy via CLI
|
||||
<ArrowUpRightIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2 bg-background"
|
||||
onClick={() => setImportModalOpen(true)}
|
||||
>
|
||||
<DownloadIcon className="size-4" />
|
||||
Import Contract
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { ClientOnly } from "components/ClientOnly/ClientOnly";
|
||||
import { Suspense } from "react";
|
||||
import { ContractTable } from "../../../../components/contract-components/tables/contract-table";
|
||||
import { DeployedContractsPageHeader } from "../DeployedContractsPageHeader";
|
||||
import { GetStartedWithContractsDeploy } from "./GetStartedWithContractsDeploy";
|
||||
import { DeployViaCLIOrImportCard } from "./DeployViaCLIOrImportCard";
|
||||
import { getSortedDeployedContracts } from "./getSortedDeployedContracts";
|
||||
|
||||
export function DeployedContractsPage(props: {
|
||||
@@ -17,10 +17,16 @@ export function DeployedContractsPage(props: {
|
||||
teamId={props.teamId}
|
||||
projectId={props.projectId}
|
||||
/>
|
||||
<div className="flex grow flex-col">
|
||||
<div className="h-6" />
|
||||
<div className="container flex max-w-7xl grow flex-col">
|
||||
<Suspense fallback={<Loading />}>
|
||||
<DeployedContractsPageAsync {...props} />
|
||||
</Suspense>
|
||||
<div className="h-8" />
|
||||
<DeployViaCLIOrImportCard
|
||||
teamId={props.teamId}
|
||||
projectId={props.projectId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -37,15 +43,6 @@ async function DeployedContractsPageAsync(props: {
|
||||
authToken: props.authToken,
|
||||
});
|
||||
|
||||
if (deployedContracts.length === 0) {
|
||||
return (
|
||||
<GetStartedWithContractsDeploy
|
||||
teamId={props.teamId}
|
||||
projectId={props.projectId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ClientOnly ssr={<Loading />}>
|
||||
<ContractTable
|
||||
|
||||
-139
@@ -1,139 +0,0 @@
|
||||
"use client";
|
||||
import { TabButtons } from "@/components/ui/tabs";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { ImportModal } from "components/contract-components/import-contract/modal";
|
||||
import { StepsCard } from "components/dashboard/StepsCard";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
export function GetStartedWithContractsDeploy(props: {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
}) {
|
||||
const steps = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: "Build, deploy or import a contract",
|
||||
description:
|
||||
"Choose between deploying your own contract or import an existing one.",
|
||||
children: (
|
||||
<DeployOptions teamId={props.teamId} projectId={props.projectId} />
|
||||
),
|
||||
completed: false, // because we only show this component if the user does not have any contracts
|
||||
},
|
||||
],
|
||||
[props.teamId, props.projectId],
|
||||
);
|
||||
|
||||
return (
|
||||
<StepsCard
|
||||
title="Get started with deploying contracts"
|
||||
description="This guide will help you to start deploying contracts on-chain in just a few minutes."
|
||||
steps={steps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type ContentItem = {
|
||||
title: string;
|
||||
description: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
type TabId = "explore" | "import" | "build" | "deploy";
|
||||
|
||||
const DeployOptions = (props: {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
}) => {
|
||||
const [showImportModal, setShowImportModal] = useState(false);
|
||||
const router = useDashboardRouter();
|
||||
const trackEvent = useTrack();
|
||||
|
||||
const content: Record<TabId, ContentItem> = useMemo(
|
||||
() => ({
|
||||
explore: {
|
||||
title: "Ready-to-deploy",
|
||||
description:
|
||||
"Pick from our library of ready-to-deploy contracts and deploy to any EVM chain in just 1-click.",
|
||||
href: "/explore",
|
||||
},
|
||||
import: {
|
||||
title: "Import",
|
||||
description:
|
||||
"Import an already deployed contract to build apps on top of contract using thirdweb tools.",
|
||||
onClick: () => setShowImportModal(true),
|
||||
},
|
||||
build: {
|
||||
title: "Build your own",
|
||||
description:
|
||||
"Get started with the Solidity SDK to create custom contracts specific to your use case.",
|
||||
href: "/build",
|
||||
},
|
||||
deploy: {
|
||||
title: "Deploy from source",
|
||||
description:
|
||||
"Deploy your contract by using our interactive CLI. (Supports Hardhat, Forge, Truffle, and more)",
|
||||
href: "https://portal.thirdweb.com/cli",
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<TabId>("explore");
|
||||
const activeTabContent = content[activeTab];
|
||||
|
||||
return (
|
||||
<div className="mt-3">
|
||||
<ImportModal
|
||||
isOpen={showImportModal}
|
||||
onClose={() => {
|
||||
setShowImportModal(false);
|
||||
}}
|
||||
teamId={props.teamId}
|
||||
projectId={props.projectId}
|
||||
/>
|
||||
|
||||
<TabButtons
|
||||
tabs={Object.entries(content).map(([key, value]) => ({
|
||||
key,
|
||||
name: value.title,
|
||||
isActive: activeTab === key,
|
||||
onClick: () => setActiveTab(key as TabId),
|
||||
}))}
|
||||
tabClassName="font-medium"
|
||||
tabContainerClassName="gap-1"
|
||||
/>
|
||||
|
||||
<button
|
||||
className="mt-3 flex w-full cursor-pointer items-center gap-2 rounded-lg border border-border bg-card p-4 hover:border-active-border"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
activeTabContent.onClick?.();
|
||||
if (activeTabContent.href) {
|
||||
router.push(activeTabContent.href);
|
||||
}
|
||||
|
||||
trackEvent({
|
||||
category: "your_contracts",
|
||||
action: "click",
|
||||
label: "deploy_options",
|
||||
type: activeTab,
|
||||
href: null,
|
||||
isExternal: false,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h4 className="text-start font-semibold text-lg">
|
||||
{activeTabContent.title}
|
||||
</h4>
|
||||
<p className="text-start text-muted-foreground text-sm">
|
||||
{activeTabContent.description}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
import { mobileViewport } from "../../../stories/utils";
|
||||
import { AccountSettingsPageUI } from "./AccountSettingsPageUI";
|
||||
|
||||
@@ -147,7 +146,6 @@ function Variants() {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}}
|
||||
/>
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { LinkedWallet } from "@/api/linked-wallets";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Toaster } from "sonner";
|
||||
import { ThirdwebProvider } from "thirdweb/react";
|
||||
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { LinkWalletUI } from "./LinkWalletUI";
|
||||
@@ -91,8 +90,6 @@ function Variants() {
|
||||
accountEmail="[email protected]"
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// app/providers.tsx
|
||||
"use client";
|
||||
import posthog from "posthog-js";
|
||||
import { PostHogProvider as PHProvider } from "posthog-js/react";
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
posthog.init(
|
||||
process.env.NEXT_PUBLIC_POSTHOG_API_KEY ||
|
||||
"phc_hKK4bo8cHZrKuAVXfXGpfNSLSJuucUnguAgt2j6dgSV",
|
||||
{
|
||||
api_host: "https://a.thirdweb.com",
|
||||
autocapture: true,
|
||||
debug: false,
|
||||
capture_pageview: true,
|
||||
disable_session_recording: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
||||
return <PHProvider client={posthog}>{children}</PHProvider>;
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { mobileViewport } from "../../../../../stories/utils";
|
||||
import { JoinTeamPageUI } from "./JoinTeamPage";
|
||||
|
||||
const meta = {
|
||||
@@ -16,17 +14,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div>
|
||||
@@ -36,7 +27,6 @@ function Story() {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}}
|
||||
/>
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,10 @@ import NextTopLoader from "nextjs-toploader";
|
||||
import { Suspense } from "react";
|
||||
import { OpCreditsGrantedModalWrapperServer } from "../components/onboarding/OpCreditsGrantedModalWrapperServer";
|
||||
import { PosthogIdentifierServer } from "../components/wallets/PosthogIdentifierServer";
|
||||
import { PHProvider } from "../lib/posthog/Posthog";
|
||||
import { PosthogHeadSetup } from "../lib/posthog/PosthogHeadSetup";
|
||||
import { PostHogPageView } from "../lib/posthog/PosthogPageView";
|
||||
import { EnsureValidConnectedWalletLoginServer } from "./components/EnsureValidConnectedWalletLogin/EnsureValidConnectedWalletLoginServer";
|
||||
import { PostHogProvider } from "./components/root-providers";
|
||||
import { AppRouterProviders } from "./providers";
|
||||
|
||||
const fontSans = Inter({
|
||||
@@ -63,8 +65,10 @@ export default function RootLayout({
|
||||
customDomain="https://pl.thirdweb.com"
|
||||
selfHosted
|
||||
/>
|
||||
<PosthogHeadSetup />
|
||||
</head>
|
||||
<PostHogProvider>
|
||||
<PHProvider>
|
||||
<PostHogPageView />
|
||||
<body
|
||||
className={cn(
|
||||
"bg-background font-sans antialiased",
|
||||
@@ -91,7 +95,7 @@ export default function RootLayout({
|
||||
showSpinner={false}
|
||||
/>
|
||||
</body>
|
||||
</PostHogProvider>
|
||||
</PHProvider>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { teamStub } from "../../../../stories/stubs";
|
||||
import { storybookLog } from "../../../../stories/utils";
|
||||
@@ -59,7 +58,6 @@ function Story(props: {
|
||||
return teamStub("foo", "free");
|
||||
}}
|
||||
/>
|
||||
<Toaster richColors />
|
||||
</TeamOnboardingLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,35 +2,51 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { subDays } from "date-fns";
|
||||
import { ThirdwebProvider } from "thirdweb/react";
|
||||
import { accountStub, randomLorem } from "../../../../../stories/stubs";
|
||||
import { BadgeContainer, mobileViewport } from "../../../../../stories/utils";
|
||||
import { ChatPageLayout } from "../../components/ChatPageLayout";
|
||||
import { ChatHistoryPageUI } from "./ChatHistoryPage";
|
||||
|
||||
const meta = {
|
||||
title: "Nebula/history",
|
||||
component: Story,
|
||||
component: Variant,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Story>;
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ThirdwebProvider>
|
||||
<Story />
|
||||
</ThirdwebProvider>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof Variant>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const TenChats: Story = {
|
||||
args: {
|
||||
length: 10,
|
||||
},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
export const ZeroChats: Story = {
|
||||
args: {
|
||||
length: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const OneChat: Story = {
|
||||
args: {
|
||||
length: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const PrefillSearch: Story = {
|
||||
args: {
|
||||
length: 10,
|
||||
},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
prefillSearch: "xxxxxxxxxxxx",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -51,30 +67,6 @@ function createRandomSessions(length: number) {
|
||||
return sessions;
|
||||
}
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<div className="flex flex-col gap-10 py-10">
|
||||
<BadgeContainer label="10 chats">
|
||||
<Variant length={10} />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="0 chats">
|
||||
<Variant length={0} />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="1 chat">
|
||||
<Variant length={1} />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="No search result">
|
||||
<Variant length={10} prefillSearch="xxxxxxxxxxxx" />
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Variant(props: {
|
||||
length: number;
|
||||
prefillSearch?: string;
|
||||
@@ -85,7 +77,6 @@ function Variant(props: {
|
||||
accountAddress="0x1234567890"
|
||||
authToken="xxxxxxxx"
|
||||
sessions={createRandomSessions(props.length)}
|
||||
className="h-[700px] border-b lg:h-[800px]"
|
||||
>
|
||||
<ChatHistoryPageUI
|
||||
sessions={createRandomSessions(props.length)}
|
||||
|
||||
@@ -1,32 +1,51 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Toaster } from "sonner";
|
||||
import { ThirdwebProvider } from "thirdweb/react";
|
||||
import { accountStub, randomLorem } from "../../../../stories/stubs";
|
||||
import { BadgeContainer, mobileViewport } from "../../../../stories/utils";
|
||||
import type { TruncatedSessionInfo } from "../api/types";
|
||||
import { ChatPageLayout } from "./ChatPageLayout";
|
||||
|
||||
const meta = {
|
||||
title: "Nebula/ChatPageLayout",
|
||||
component: Story,
|
||||
component: Variant,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ThirdwebProvider>
|
||||
<Story />
|
||||
</ThirdwebProvider>
|
||||
),
|
||||
],
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Story>;
|
||||
} satisfies Meta<typeof Variant>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
export const NoRecentChats: Story = {
|
||||
args: {
|
||||
sessions: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
export const ThreeChats: Story = {
|
||||
args: {
|
||||
sessions: generateRandomSessions(3),
|
||||
},
|
||||
};
|
||||
|
||||
export const ThirtyChats: Story = {
|
||||
args: {
|
||||
sessions: generateRandomSessions(30),
|
||||
},
|
||||
};
|
||||
|
||||
export const NoEmail: Story = {
|
||||
args: {
|
||||
sessions: generateRandomSessions(30),
|
||||
noEmail: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -39,48 +58,26 @@ function generateRandomSessions(count: number): TruncatedSessionInfo[] {
|
||||
}));
|
||||
}
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<div className="flex flex-col gap-10 py-10 lg:container">
|
||||
<Variant label="No Recent Chats" sessions={[]} />
|
||||
<Variant label="3 Chats" sessions={generateRandomSessions(3)} />
|
||||
<Variant label="30 Chats" sessions={generateRandomSessions(30)} />
|
||||
<Variant
|
||||
label="No Email"
|
||||
sessions={generateRandomSessions(30)}
|
||||
noEmail
|
||||
/>
|
||||
</div>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Variant(props: {
|
||||
label: string;
|
||||
sessions: TruncatedSessionInfo[];
|
||||
noEmail?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<BadgeContainer label={props.label}>
|
||||
<ChatPageLayout
|
||||
accountAddress="0xC569B9FD77d132e10954cA5E6EF617414e314b11"
|
||||
authToken="xxxxx"
|
||||
sessions={props.sessions}
|
||||
className="h-[200px] lg:h-[800px] lg:border"
|
||||
account={accountStub(
|
||||
props.noEmail
|
||||
? {
|
||||
email: undefined,
|
||||
}
|
||||
: undefined,
|
||||
)}
|
||||
>
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
CHILDREN
|
||||
</div>
|
||||
</ChatPageLayout>
|
||||
<Toaster richColors />
|
||||
</BadgeContainer>
|
||||
<ChatPageLayout
|
||||
accountAddress="0xC569B9FD77d132e10954cA5E6EF617414e314b11"
|
||||
authToken="xxxxx"
|
||||
sessions={props.sessions}
|
||||
account={accountStub(
|
||||
props.noEmail
|
||||
? {
|
||||
email: undefined,
|
||||
}
|
||||
: undefined,
|
||||
)}
|
||||
>
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
CHILDREN
|
||||
</div>
|
||||
</ChatPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { BadgeContainer, mobileViewport } from "../../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../../stories/utils";
|
||||
import { ChatBar } from "./ChatBar";
|
||||
|
||||
const meta = {
|
||||
@@ -15,20 +15,13 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-[800px] flex-col gap-14 py-10">
|
||||
<div className="container flex max-w-6xl flex-col gap-14 py-10">
|
||||
<BadgeContainer label="Not Streaming">
|
||||
<ChatBar
|
||||
abortChatStream={() => {}}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Toaster } from "sonner";
|
||||
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
|
||||
import { accountStub, randomLorem } from "../../../../stories/stubs";
|
||||
import { BadgeContainer, mobileViewport } from "../../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../../stories/utils";
|
||||
import { type ChatMessage, Chats } from "./Chats";
|
||||
|
||||
const meta = {
|
||||
@@ -19,17 +18,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
const markdownExample = `\
|
||||
# Heading 1
|
||||
## Heading 2
|
||||
@@ -245,8 +237,6 @@ function Story() {
|
||||
]}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
import { BadgeContainer, mobileViewport } from "../../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../../stories/utils";
|
||||
import type { NebulaContext } from "../api/chat";
|
||||
import ContextFiltersButton from "./ContextFilters";
|
||||
|
||||
@@ -18,20 +17,13 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-[1000px] flex-col gap-8 py-10 lg:p-10">
|
||||
<div className="container flex max-w-6xl flex-col gap-8 py-10">
|
||||
<Variant contextFilters={undefined} label="No Filters Set" />
|
||||
|
||||
<Variant
|
||||
@@ -65,7 +57,6 @@ function Story() {
|
||||
}}
|
||||
label="chains + wallet"
|
||||
/>
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-9
@@ -1,5 +1,4 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { mobileViewport } from "../../../../stories/utils";
|
||||
import { EmptyStateChatPageContent } from "./EmptyStateChatPageContent";
|
||||
|
||||
const meta = {
|
||||
@@ -15,17 +14,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-[800px] grow flex-col justify-center overflow-hidden">
|
||||
|
||||
+2
-9
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
|
||||
import { accountStub } from "../../../../stories/stubs";
|
||||
import { BadgeContainer, mobileViewport } from "../../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../../stories/utils";
|
||||
import {
|
||||
ExecuteTransactionCardLayout,
|
||||
type TxStatus,
|
||||
@@ -22,17 +22,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
const client = getThirdwebClient();
|
||||
|
||||
const exampleTxHash =
|
||||
|
||||
-1
@@ -7,7 +7,6 @@ import {
|
||||
const meta = {
|
||||
title: "Banners/Billing Alerts",
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
|
||||
@@ -88,10 +88,10 @@ export async function TotalSponsoredChartCardUI({
|
||||
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">
|
||||
<h3 className="mb-0.5 font-semibold text-xl tracking-tight">
|
||||
{title || "Total Sponsored"}
|
||||
</h3>
|
||||
<p className="text-muted-foreground"> {description}</p>
|
||||
<p className="text-muted-foreground text-sm"> {description}</p>
|
||||
<BarChart
|
||||
isCurrency
|
||||
chartConfig={chartConfig}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getProjects } from "@/api/projects";
|
||||
import { getTeamNebulaWaitList, getTeams } from "@/api/team";
|
||||
import { getTeams } from "@/api/team";
|
||||
import { AppFooter } from "@/components/blocks/app-footer";
|
||||
import { TabPathLinks } from "@/components/ui/tabs";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -39,9 +39,6 @@ export default async function TeamLayout(props: {
|
||||
})),
|
||||
);
|
||||
|
||||
const isOnNebulaWaitList = (await getTeamNebulaWaitList(team.slug))
|
||||
?.onWaitlist;
|
||||
|
||||
return (
|
||||
<div className="flex h-full grow flex-col">
|
||||
<AnnouncementBanner />
|
||||
@@ -74,14 +71,6 @@ export default async function TeamLayout(props: {
|
||||
path: `/team/${params.team_slug}/~/ecosystem`,
|
||||
name: "Ecosystems",
|
||||
},
|
||||
...(isOnNebulaWaitList
|
||||
? [
|
||||
{
|
||||
path: `/team/${params.team_slug}/~/nebula`,
|
||||
name: "Nebula",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
path: `/team/${params.team_slug}/~/usage`,
|
||||
name: "Usage",
|
||||
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import type { EcosystemWalletStats } from "types/analytics";
|
||||
import { EcosystemWalletUsersChartCard } from "./EcosystemWalletUsersChartCard";
|
||||
|
||||
const meta = {
|
||||
title: "Ecosystem/Analytics/EcosystemWalletUsersChartCard",
|
||||
component: EcosystemWalletUsersChartCard,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="container max-w-7xl py-10">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof EcosystemWalletUsersChartCard>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const authMethods = [
|
||||
"Email",
|
||||
"Google",
|
||||
"Apple",
|
||||
"Discord",
|
||||
"Twitter",
|
||||
"GitHub",
|
||||
"Facebook",
|
||||
"Twitch",
|
||||
"LinkedIn",
|
||||
"TikTok",
|
||||
"Coinbase",
|
||||
"MetaMask",
|
||||
];
|
||||
|
||||
function ecosystemWalletStatsStub(
|
||||
length: number,
|
||||
startDate = new Date(2024, 11, 1),
|
||||
): EcosystemWalletStats[] {
|
||||
const stats: EcosystemWalletStats[] = [];
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const date = new Date(startDate);
|
||||
date.setDate(date.getDate() + i);
|
||||
const formattedDate = date.toISOString().split("T")[0] || "";
|
||||
|
||||
// each day, we pick between 1 and 4 auth methods
|
||||
const authMethodsToPick = Math.floor(Math.random() * 4) + 1;
|
||||
|
||||
for (let j = 0; j < authMethodsToPick; j++) {
|
||||
const authMethod =
|
||||
authMethods[Math.floor(Math.random() * authMethods.length)];
|
||||
stats.push({
|
||||
date: formattedDate,
|
||||
authenticationMethod: authMethod || "MetaMask",
|
||||
uniqueWalletsConnected: Math.floor(Math.random() * 1000) + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
// Empty data state
|
||||
export const EmptyData: Story = {
|
||||
args: {
|
||||
ecosystemWalletStats: [],
|
||||
isPending: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Loading state
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
ecosystemWalletStats: [],
|
||||
isPending: true,
|
||||
},
|
||||
};
|
||||
|
||||
// 30 days of data
|
||||
export const ThirtyDaysData: Story = {
|
||||
args: {
|
||||
ecosystemWalletStats: ecosystemWalletStatsStub(30),
|
||||
isPending: false,
|
||||
},
|
||||
};
|
||||
|
||||
// 60 days of data
|
||||
export const SixtyDaysData: Story = {
|
||||
args: {
|
||||
ecosystemWalletStats: ecosystemWalletStatsStub(60),
|
||||
isPending: false,
|
||||
},
|
||||
};
|
||||
|
||||
// 120 days of data
|
||||
export const OneHundredTwentyDaysData: Story = {
|
||||
args: {
|
||||
ecosystemWalletStats: ecosystemWalletStatsStub(120),
|
||||
isPending: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Data with lots of authentication methods to test the "Others" category
|
||||
export const ManyAuthMethods: Story = {
|
||||
args: {
|
||||
ecosystemWalletStats: (() => {
|
||||
// Generate data with 15 different auth methods to test "Others" category
|
||||
const basicData = ecosystemWalletStatsStub(30);
|
||||
|
||||
return basicData.map((item, index) => ({
|
||||
...item,
|
||||
authenticationMethod:
|
||||
authMethods[index % authMethods.length] || "MetaMask",
|
||||
}));
|
||||
})(),
|
||||
isPending: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Zero values test
|
||||
export const ZeroValues: Story = {
|
||||
args: {
|
||||
ecosystemWalletStats: ecosystemWalletStatsStub(30).map((stat) => ({
|
||||
...stat,
|
||||
uniqueWalletsConnected: 0,
|
||||
})),
|
||||
isPending: false,
|
||||
},
|
||||
};
|
||||
+81
-135
@@ -1,26 +1,15 @@
|
||||
"use client";
|
||||
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
|
||||
import {
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import {
|
||||
EmptyChartState,
|
||||
LoadingChartState,
|
||||
} from "components/analytics/empty-chart-state";
|
||||
import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";
|
||||
import type { ChartConfig } from "@/components/ui/chart";
|
||||
import { ReactIcon } from "components/icons/brand-icons/ReactIcon";
|
||||
import { TypeScriptIcon } from "components/icons/brand-icons/TypeScriptIcon";
|
||||
import { UnityIcon } from "components/icons/brand-icons/UnityIcon";
|
||||
import { DocLink } from "components/shared/DocLink";
|
||||
import { format } from "date-fns";
|
||||
import { formatTickerNumber } from "lib/format-utils";
|
||||
import { formatDate } from "date-fns";
|
||||
import { useMemo } from "react";
|
||||
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
||||
import type { EcosystemWalletStats } from "types/analytics";
|
||||
import { formatTickerNumber } from "../../../../../../../../../../lib/format-utils";
|
||||
|
||||
type ChartData = Record<string, number> & {
|
||||
time: string; // human readable date
|
||||
@@ -47,7 +36,7 @@ export function EcosystemWalletUsersChartCard(props: {
|
||||
// if no data for current day - create new entry
|
||||
if (!chartData) {
|
||||
_chartDataMap.set(stat.date, {
|
||||
time: format(new Date(stat.date), "MMM dd"),
|
||||
time: stat.date,
|
||||
[authenticationMethod || defaultLabel]: stat.uniqueWalletsConnected,
|
||||
} as ChartData);
|
||||
} else if (chartData) {
|
||||
@@ -114,128 +103,85 @@ export function EcosystemWalletUsersChartCard(props: {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative w-full rounded-lg border border-border bg-card p-4 md:p-6">
|
||||
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
|
||||
Unique Users
|
||||
</h3>
|
||||
<p className="mb-3 text-muted-foreground text-sm">
|
||||
The total number of active users in your ecosystem for each period.
|
||||
</p>
|
||||
<ThirdwebBarChart
|
||||
chartClassName="aspect-[1.5] lg:aspect-[3.5]"
|
||||
customHeader={
|
||||
<div className="relative px-6 pt-6">
|
||||
<h3 className="mb-1 font-semibold text-xl tracking-tight md:text-2xl">
|
||||
Unique Users
|
||||
</h3>
|
||||
<p className="mb-3 text-muted-foreground text-sm">
|
||||
The total number of active users in your ecosystem for each period.
|
||||
</p>
|
||||
|
||||
<div className="top-6 right-6 mb-4 grid grid-cols-2 items-center gap-2 md:absolute md:mb-0 md:flex">
|
||||
<ExportToCSVButton
|
||||
className="bg-background"
|
||||
fileName="Connect Wallets"
|
||||
disabled={disableActions}
|
||||
getData={async () => {
|
||||
// Shows the number of each type of wallet connected on all dates
|
||||
const header = ["Date", ...uniqueAuthMethods];
|
||||
const rows = chartData.map((data) => {
|
||||
const { time, ...rest } = data;
|
||||
return [
|
||||
time,
|
||||
...uniqueAuthMethods.map((w) => (rest[w] || 0).toString()),
|
||||
];
|
||||
});
|
||||
return { header, rows };
|
||||
}}
|
||||
<div className="top-6 right-6 mb-4 grid grid-cols-2 items-center gap-2 md:absolute md:mb-0 md:flex">
|
||||
<ExportToCSVButton
|
||||
className="bg-background"
|
||||
fileName="Connect Wallets"
|
||||
disabled={disableActions}
|
||||
getData={async () => {
|
||||
// Shows the number of each type of wallet connected on all dates
|
||||
const header = ["Date", ...uniqueAuthMethods];
|
||||
const rows = chartData.map((data) => {
|
||||
const { time, ...rest } = data;
|
||||
return [
|
||||
time,
|
||||
...uniqueAuthMethods.map((w) => (rest[w] || 0).toString()),
|
||||
];
|
||||
});
|
||||
return { header, rows };
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
config={chartConfig}
|
||||
data={chartData}
|
||||
isPending={props.isPending}
|
||||
emptyChartState={<EcosystemWalletUsersEmptyChartState />}
|
||||
showLegend
|
||||
hideLabel={false}
|
||||
variant="stacked"
|
||||
toolTipLabelFormatter={(_v, item) => {
|
||||
if (Array.isArray(item)) {
|
||||
const time = item[0].payload.time as number;
|
||||
return formatDate(new Date(time), "MMM d, yyyy");
|
||||
}
|
||||
return undefined;
|
||||
}}
|
||||
toolTipValueFormatter={(value) => formatTickerNumber(Number(value))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function EcosystemWalletUsersEmptyChartState() {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center px-4">
|
||||
<span className="mb-6 text-center text-lg">
|
||||
Connect users to your app with social logins
|
||||
</span>
|
||||
<div className="flex max-w-md flex-wrap items-center justify-center gap-x-6 gap-y-4">
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/typescript/v5/ecosystemWallet"
|
||||
label="TypeScript"
|
||||
icon={TypeScriptIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/react/v5/ecosystem-wallet/get-started"
|
||||
label="React"
|
||||
icon={ReactIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/react/v5/ecosystem-wallet/get-started"
|
||||
label="React Native"
|
||||
icon={ReactIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/unity/v5/wallets/ecosystem-wallet"
|
||||
label="Unity"
|
||||
icon={UnityIcon}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="h-[250px] w-full md:h-[350px]"
|
||||
>
|
||||
{props.isPending ? (
|
||||
<LoadingChartState />
|
||||
) : chartData.length === 0 ||
|
||||
uniqueAuthMethods.every((authMethod) =>
|
||||
chartData.every((data) => data[authMethod] === 0),
|
||||
) ? (
|
||||
<EmptyChartState>
|
||||
<div className="flex flex-col items-center justify-center px-4">
|
||||
<span className="mb-6 text-center text-lg">
|
||||
Connect users to your app with social logins
|
||||
</span>
|
||||
<div className="flex max-w-md flex-wrap items-center justify-center gap-x-6 gap-y-4">
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/typescript/v5/ecosystemWallet"
|
||||
label="TypeScript"
|
||||
icon={TypeScriptIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/react/v5/ecosystem-wallet/get-started"
|
||||
label="React"
|
||||
icon={ReactIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/react/v5/ecosystem-wallet/get-started"
|
||||
label="React Native"
|
||||
icon={ReactIcon}
|
||||
/>
|
||||
<DocLink
|
||||
link="https://portal.thirdweb.com/unity/v5/wallets/ecosystem-wallet"
|
||||
label="Unity"
|
||||
icon={UnityIcon}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</EmptyChartState>
|
||||
) : (
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
top: 20,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
/>
|
||||
|
||||
<YAxis
|
||||
dataKey={(data) =>
|
||||
Object.entries(data)
|
||||
.filter(([key]) => key !== "time")
|
||||
.map(([, value]) => value)
|
||||
.reduce((acc, current) => Number(acc) + Number(current), 0)
|
||||
}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => formatTickerNumber(value)}
|
||||
/>
|
||||
|
||||
<ChartTooltip
|
||||
cursor={true}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
valueFormatter={(value) => formatTickerNumber(Number(value))}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
{uniqueAuthMethods.map((authMethod) => {
|
||||
return (
|
||||
<Bar
|
||||
key={authMethod}
|
||||
dataKey={authMethod}
|
||||
fill={chartConfig[authMethod]?.color}
|
||||
radius={4}
|
||||
stackId="a"
|
||||
strokeWidth={1}
|
||||
className="stroke-background"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</BarChart>
|
||||
)}
|
||||
</ChartContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+5
-3
@@ -1,4 +1,4 @@
|
||||
import { Stat } from "components/analytics/stat";
|
||||
import { StatCard } from "components/analytics/stat";
|
||||
import { ActivityIcon, UserIcon } from "lucide-react";
|
||||
import type { EcosystemWalletStats } from "types/analytics";
|
||||
|
||||
@@ -28,15 +28,17 @@ export function EcosystemWalletsSummary(props: {
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4 lg:gap-6">
|
||||
<Stat
|
||||
<StatCard
|
||||
label="Total Users"
|
||||
value={allTimeStats?.uniqueWalletsConnected || 0}
|
||||
icon={ActivityIcon}
|
||||
isPending={false}
|
||||
/>
|
||||
<Stat
|
||||
<StatCard
|
||||
label="Monthly Active Users"
|
||||
value={monthlyStats?.uniqueWalletsConnected || 0}
|
||||
icon={UserIcon}
|
||||
isPending={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+11
-3
@@ -1,4 +1,3 @@
|
||||
import type { Range } from "components/analytics/date-range-selector";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTeamBySlug } from "../../../../../../../../../@/api/team";
|
||||
import { getAuthToken } from "../../../../../../../../api/lib/getAuthToken";
|
||||
@@ -12,7 +11,8 @@ export default async function Page(props: {
|
||||
}>;
|
||||
searchParams: Promise<{
|
||||
interval?: "day" | "week";
|
||||
range?: Range;
|
||||
from?: string;
|
||||
to?: string;
|
||||
}>;
|
||||
}) {
|
||||
const [params, searchParams] = await Promise.all([
|
||||
@@ -45,7 +45,15 @@ export default async function Page(props: {
|
||||
ecosystemSlug={ecosystem.slug}
|
||||
teamId={team.id}
|
||||
interval={searchParams.interval || "week"}
|
||||
range={searchParams.range}
|
||||
range={
|
||||
searchParams.from && searchParams.to
|
||||
? {
|
||||
from: new Date(searchParams.from),
|
||||
to: new Date(searchParams.to),
|
||||
type: "custom",
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
-3
@@ -4,9 +4,6 @@ import { EngineImportCardUI } from "./EngineImportPage";
|
||||
const meta: Meta<typeof EngineImportCardUI> = {
|
||||
title: "Engine/general/import",
|
||||
component: EngineImportCardUI,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="py-10">
|
||||
|
||||
-3
@@ -1,13 +1,11 @@
|
||||
import type { EngineInstance } from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Toaster } from "sonner";
|
||||
import { EngineInstancesTableUI } from "./engine-instances-table";
|
||||
|
||||
const meta: Meta<typeof Story> = {
|
||||
title: "Engine/general/instances",
|
||||
component: Story,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
@@ -16,7 +14,6 @@ const meta: Meta<typeof Story> = {
|
||||
(StoryInstance) => (
|
||||
<div className="container max-w-[1154px] py-10">
|
||||
<StoryInstance />
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
|
||||
+4
-13
@@ -4,16 +4,15 @@ import type {
|
||||
} from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Toaster } from "sonner";
|
||||
import {
|
||||
createEngineAlertRuleStub,
|
||||
createEngineNotificationChannelStub,
|
||||
} from "stories/stubs";
|
||||
import { BadgeContainer, mobileViewport } from "stories/utils";
|
||||
import { BadgeContainer } from "stories/utils";
|
||||
import { ManageEngineAlertsSectionUI } from "./ManageEngineAlerts";
|
||||
|
||||
const meta = {
|
||||
title: "Engine/Alerts/Manage Alerts",
|
||||
title: "Engine/Alerts/ManageAlerts",
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
@@ -25,17 +24,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
const createEngineAlertMutation = useMutation({
|
||||
mutationFn: async (values: CreateNotificationChannelInput) => {
|
||||
@@ -63,7 +55,7 @@ function Story() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container flex max-w-[1154px] flex-col gap-14 py-6">
|
||||
<div className="container flex max-w-6xl flex-col gap-14 py-6">
|
||||
<BadgeContainer label="2 Alerts">
|
||||
<ManageEngineAlertsSectionUI
|
||||
isLoading={false}
|
||||
@@ -125,7 +117,6 @@ function Story() {
|
||||
deleteAlert={deleteAlert}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-13
@@ -1,10 +1,9 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Toaster } from "sonner";
|
||||
import {
|
||||
createEngineAlertRuleStub,
|
||||
createEngineAlertStub,
|
||||
} from "stories/stubs";
|
||||
import { BadgeContainer, mobileViewport } from "stories/utils";
|
||||
import { BadgeContainer } from "stories/utils";
|
||||
import { RecentEngineAlertsSectionUI } from "./RecentEngineAlerts";
|
||||
|
||||
const meta = {
|
||||
@@ -20,20 +19,13 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-[1154px] flex-col gap-14 py-6">
|
||||
<div className="container flex max-w-6xl flex-col gap-14 py-10">
|
||||
<BadgeContainer label="3 Alerts">
|
||||
<RecentEngineAlertsSectionUI
|
||||
isLoading={false}
|
||||
@@ -68,8 +60,6 @@ function Story() {
|
||||
onAlertsUpdated={() => {}}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { getTeamBySlug } from "@/api/team";
|
||||
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
|
||||
import { loginRedirect } from "../../../../../login/loginRedirect";
|
||||
import { NebulaAnalyticsPage } from "../../../[project_slug]/nebula/components/analytics/nebula-analytics-page";
|
||||
import { NebulaWaitListPage } from "../../../[project_slug]/nebula/components/nebula-waitlist-page";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
team_slug: string;
|
||||
}>;
|
||||
searchParams: Promise<{
|
||||
from: string | undefined | string[];
|
||||
to: string | undefined | string[];
|
||||
interval: string | undefined | string[];
|
||||
}>;
|
||||
}) {
|
||||
const [params, searchParams] = await Promise.all([
|
||||
props.params,
|
||||
props.searchParams,
|
||||
]);
|
||||
|
||||
const [authToken, team] = await Promise.all([
|
||||
getAuthToken(),
|
||||
getTeamBySlug(params.team_slug),
|
||||
]);
|
||||
|
||||
if (!team || !authToken) {
|
||||
loginRedirect(`/team/${params.team_slug}/~/nebula`);
|
||||
}
|
||||
|
||||
const hasNebulaAccess = team.enabledScopes.includes("nebula");
|
||||
|
||||
if (hasNebulaAccess) {
|
||||
return (
|
||||
<NebulaAnalyticsPage
|
||||
teamId={team.id}
|
||||
authToken={authToken}
|
||||
searchParams={searchParams}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <NebulaWaitListPage team={team} />;
|
||||
}
|
||||
+3
-13
@@ -1,10 +1,7 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { addDays } from "date-fns";
|
||||
import { teamStub, teamSubscriptionsStub } from "stories/stubs";
|
||||
import {
|
||||
BadgeContainer,
|
||||
mobileViewport,
|
||||
} from "../../../../../../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../../../../../../stories/utils";
|
||||
import { PlanInfoCard } from "./PlanInfoCard";
|
||||
|
||||
const meta = {
|
||||
@@ -20,17 +17,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
const team = teamStub("foo", "growth");
|
||||
const zeroUsageOnDemandSubs = teamSubscriptionsStub("plan:growth");
|
||||
@@ -74,7 +64,7 @@ function Story() {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="container flex max-w-[1130px] flex-col gap-12 lg:p-10">
|
||||
<div className="container flex max-w-6xl flex-col gap-12 py-10">
|
||||
<BadgeContainer label="On-demand Subscriptions with 0 usage">
|
||||
<PlanInfoCard
|
||||
team={team}
|
||||
|
||||
+11
-21
@@ -5,9 +5,9 @@ import { BillingPortalButton } from "@/components/billing";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { differenceInDays, isAfter } from "date-fns";
|
||||
import { format } from "date-fns/format";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { getValidTeamPlan } from "../../../../../../components/TeamHeader/getValidTeamPlan";
|
||||
@@ -37,18 +37,21 @@ export function PlanInfoCard(props: {
|
||||
<div className="rounded-lg border border-border bg-card">
|
||||
<div className="flex flex-col gap-4 p-4 lg:flex-row lg:items-center lg:justify-between lg:p-6">
|
||||
<div>
|
||||
<div className="flex flex-col items-start gap-1">
|
||||
<h3 className="font-semibold text-2xl capitalize tracking-tight">
|
||||
{validPlan} Plan
|
||||
</h3>
|
||||
<div className="flex flex-col items-start gap-0.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold text-2xl capitalize tracking-tight">
|
||||
{validPlan} Plan
|
||||
</h3>
|
||||
{trialEndsInFuture && <Badge variant="warning">Trial</Badge>}
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Click on "Manage Billing" to view your invoices, update your
|
||||
payment method, or edit your billing details.
|
||||
</p>
|
||||
{trialEndsInFuture && <Badge>Trial</Badge>}
|
||||
</div>
|
||||
{trialEndsAfterDays > 0 && (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<p className="mt-4 flex items-center gap-1 text-foreground text-sm">
|
||||
<InfoIcon className="size-3" />
|
||||
Your trial ends in {trialEndsAfterDays} days
|
||||
</p>
|
||||
)}
|
||||
@@ -56,6 +59,7 @@ export function PlanInfoCard(props: {
|
||||
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
{/* go to invoices page */}
|
||||
|
||||
<Button asChild variant="outline">
|
||||
<Link href={`/team/${team.slug}/~/settings/invoices`}>
|
||||
View Invoices
|
||||
@@ -73,20 +77,6 @@ export function PlanInfoCard(props: {
|
||||
>
|
||||
Manage Billing
|
||||
</BillingPortalButton>
|
||||
|
||||
{isActualFreePlan && (
|
||||
<Button asChild variant="outline">
|
||||
<TrackedLinkTW
|
||||
category="account"
|
||||
href="/pricing"
|
||||
label="pricing-plans"
|
||||
target="_blank"
|
||||
className="inline-flex items-center gap-2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
View Pricing
|
||||
</TrackedLinkTW>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+2
-12
@@ -1,8 +1,6 @@
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { teamStub } from "../../../../../../../stories/stubs";
|
||||
import { mobileViewport } from "../../../../../../../stories/utils";
|
||||
import {
|
||||
DeleteTeamCard,
|
||||
LeaveTeamCard,
|
||||
@@ -22,22 +20,15 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
const testTeam = teamStub("foo", "free");
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-[1100px] px-4 py-6">
|
||||
<div className="container max-w-6xl py-10">
|
||||
<TeamGeneralSettingsPageUI
|
||||
team={testTeam}
|
||||
updateTeamImage={async () => {
|
||||
@@ -53,7 +44,6 @@ function Story() {
|
||||
}}
|
||||
/>
|
||||
<ComponentVariants />
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+2
-13
@@ -1,11 +1,9 @@
|
||||
import type { TeamAccountRole } from "@/api/team-members";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { teamStub } from "../../../../../../../stories/stubs";
|
||||
import { mobileViewport } from "../../../../../../../stories/utils";
|
||||
import { InviteSection } from "./InviteSection";
|
||||
|
||||
const meta = {
|
||||
@@ -21,17 +19,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
const TEAM_CONFIGS = [
|
||||
{ id: "free", label: "Free Team", team: teamStub("foo", "free") },
|
||||
{ id: "starter", label: "Starter Team", team: teamStub("foo", "starter") },
|
||||
@@ -89,7 +80,7 @@ function Story() {
|
||||
showPermissionControls && hasEditPermission === "true";
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-[1100px] px-4 py-6">
|
||||
<div className="container max-w-6xl py-10">
|
||||
<div className="mb-8 flex flex-col gap-6">
|
||||
<div>
|
||||
<h3 className="mb-3 font-medium">Team Type</h3>
|
||||
@@ -166,8 +157,6 @@ function Story() {
|
||||
inviteTeamMembers={INVITE_HANDLERS[inviteResult]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-16
@@ -1,13 +1,9 @@
|
||||
import type { TeamInvite } from "@/api/team-invites";
|
||||
import type { TeamAccountRole } from "@/api/team-members";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { teamStub } from "../../../../../../../stories/stubs";
|
||||
import {
|
||||
BadgeContainer,
|
||||
mobileViewport,
|
||||
} from "../../../../../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../../../../../stories/utils";
|
||||
import { ManageInvitesSection } from "./ManageInvitesSection";
|
||||
|
||||
const meta = {
|
||||
@@ -23,17 +19,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
const freeTeam = teamStub("foo", "free");
|
||||
|
||||
function createInviteStub(
|
||||
@@ -74,7 +63,7 @@ const deleteInviteStub = async (inviteId: string) => {
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-[1100px] px-4 py-6">
|
||||
<div className="container max-w-6xl py-10">
|
||||
<div className="flex flex-col gap-10">
|
||||
<BadgeContainer label="Has permission">
|
||||
<ManageInvitesSection
|
||||
@@ -96,8 +85,6 @@ function Story() {
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-16
@@ -1,12 +1,8 @@
|
||||
import type { TeamAccountRole, TeamMember } from "@/api/team-members";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { teamStub } from "../../../../../../../stories/stubs";
|
||||
import {
|
||||
BadgeContainer,
|
||||
mobileViewport,
|
||||
} from "../../../../../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../../../../../stories/utils";
|
||||
import { ManageMembersSection } from "./ManageMembersSection";
|
||||
|
||||
const meta = {
|
||||
@@ -22,17 +18,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
const freeTeam = teamStub("foo", "free");
|
||||
|
||||
function createMemberStub(
|
||||
@@ -81,7 +70,7 @@ const deleteMemberStub = async (memberId: string) => {
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-[1100px] px-4 py-6">
|
||||
<div className="container max-w-6xl py-10">
|
||||
<div className="flex flex-col gap-10">
|
||||
<BadgeContainer label="Has permission">
|
||||
<ManageMembersSection
|
||||
@@ -113,8 +102,6 @@ function Story() {
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
-3
@@ -13,9 +13,6 @@ import {
|
||||
const meta = {
|
||||
title: "Usage/SponsoredTransactionsTable",
|
||||
component: Variant,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="container max-w-[1154px] py-10">
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ export function SponsoredTransactionsTableUI(
|
||||
<div className="overflow-hidden rounded-lg border bg-card">
|
||||
{/* header */}
|
||||
<div className="flex flex-col justify-between gap-3 border-b px-4 py-4 lg:flex-row lg:items-center lg:px-6">
|
||||
<h2 className="font-semibold text-lg">Sponsored Transactions</h2>
|
||||
<h2 className="font-semibold text-xl">Sponsored Transactions</h2>
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center">
|
||||
{/* Filters */}
|
||||
<div className="flex gap-2">
|
||||
|
||||
+4
-4
@@ -23,13 +23,13 @@ export const UsageCard: React.FC<UsageCardProps> = ({
|
||||
totalUsage,
|
||||
}) => {
|
||||
return (
|
||||
<div className="relative flex min-h-[190px] flex-col rounded-lg border border-border bg-card p-4 lg:p-6">
|
||||
<h3 className="mb-1 font-semibold text-xl tracking-tight">{name}</h3>
|
||||
<p className="text-muted-foreground"> {description}</p>
|
||||
<div className="relative flex flex-col rounded-lg border border-border bg-card p-4 lg:p-6">
|
||||
<h3 className="mb-0.5 font-semibold text-xl tracking-tight">{name}</h3>
|
||||
<p className="text-muted-foreground text-sm"> {description}</p>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
<div className="mt-auto flex flex-col gap-1.5">
|
||||
<div className="flex flex-col gap-1 text-sm">
|
||||
{title && <p className="text-foreground">{title}</p>}
|
||||
|
||||
{total !== undefined && (
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
|
||||
export function ClientIDSection(props: {
|
||||
clientId: string;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<h3>Client ID</h3>
|
||||
<p className="mb-2 text-muted-foreground text-sm">
|
||||
Identifies your application
|
||||
</p>
|
||||
|
||||
<CopyTextButton
|
||||
textToCopy={props.clientId}
|
||||
className="!h-auto w-full max-w-[400px] justify-between truncate bg-background px-3 py-3 font-mono"
|
||||
textToShow={props.clientId}
|
||||
copyIconPosition="right"
|
||||
tooltip="Copy Client ID"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -29,7 +29,7 @@ export function IntegrateAPIKeyCodeTabs(props: {
|
||||
isActive: tab === key,
|
||||
}))}
|
||||
/>
|
||||
<div className="h-3" />
|
||||
<div className="h-2" />
|
||||
{props.tabs[tab]}
|
||||
</div>
|
||||
);
|
||||
|
||||
+2
-16
@@ -1,5 +1,4 @@
|
||||
import type { Project } from "@/api/projects";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { UnderlineLink } from "@/components/ui/UnderlineLink";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { CodeServer } from "@/components/ui/code/code.server";
|
||||
@@ -19,6 +18,7 @@ import { TypeScriptIcon } from "../../../../../../components/icons/brand-icons/T
|
||||
import { UnityIcon } from "../../../../../../components/icons/brand-icons/UnityIcon";
|
||||
import { UnrealIcon } from "../../../../../../components/icons/brand-icons/UnrealIcon";
|
||||
import { NebulaIcon } from "../../../../../nebula-app/(app)/icons/NebulaIcon";
|
||||
import { ClientIDSection } from "./ClientIDSection";
|
||||
import { IntegrateAPIKeyCodeTabs } from "./IntegrateAPIKeyCodeTabs";
|
||||
import { SecretKeySection } from "./SecretKeySection";
|
||||
|
||||
@@ -59,21 +59,7 @@ function IntegrateAPIKeySection({
|
||||
|
||||
<div className="rounded-lg border border-border bg-card p-4">
|
||||
<div className="flex flex-col gap-6 ">
|
||||
<div>
|
||||
<h3>Client ID</h3>
|
||||
<p className="mb-2 text-muted-foreground text-sm">
|
||||
Identifies your application.
|
||||
</p>
|
||||
|
||||
<CopyTextButton
|
||||
textToCopy={clientId}
|
||||
className="!h-auto w-full max-w-[350px] justify-between truncate bg-background px-3 py-3 font-mono"
|
||||
textToShow={clientId}
|
||||
copyIconPosition="right"
|
||||
tooltip="Copy Client ID"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ClientIDSection clientId={clientId} />
|
||||
{secretKeyMasked && (
|
||||
<SecretKeySection
|
||||
secretKeyMasked={secretKeyMasked}
|
||||
|
||||
+13
-3
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
import { FullWidthSidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
import { AppFooter } from "@/components/blocks/app-footer";
|
||||
import {
|
||||
BookTextIcon,
|
||||
BoxIcon,
|
||||
@@ -12,12 +11,14 @@ import { ContractIcon } from "../../../../(dashboard)/(chain)/components/server/
|
||||
import { InsightIcon } from "../../../../(dashboard)/(chain)/components/server/icons/InsightIcon";
|
||||
import { PayIcon } from "../../../../(dashboard)/(chain)/components/server/icons/PayIcon";
|
||||
import { SmartAccountIcon } from "../../../../(dashboard)/(chain)/components/server/icons/SmartAccountIcon";
|
||||
import { NebulaIcon } from "../../../../nebula-app/(app)/icons/NebulaIcon";
|
||||
|
||||
export function ProjectSidebarLayout(props: {
|
||||
layoutPath: string;
|
||||
children: React.ReactNode;
|
||||
showNebula: boolean;
|
||||
}) {
|
||||
const { layoutPath, children } = props;
|
||||
const { layoutPath, children, showNebula } = props;
|
||||
|
||||
const tracking = (label: string) => ({
|
||||
category: "project-sidebar",
|
||||
@@ -27,7 +28,6 @@ export function ProjectSidebarLayout(props: {
|
||||
|
||||
return (
|
||||
<FullWidthSidebarLayout
|
||||
footer={<AppFooter />}
|
||||
contentSidebarLinks={[
|
||||
{
|
||||
href: layoutPath,
|
||||
@@ -60,6 +60,16 @@ export function ProjectSidebarLayout(props: {
|
||||
icon: ContractIcon,
|
||||
tracking: tracking("contracts"),
|
||||
},
|
||||
...(showNebula
|
||||
? [
|
||||
{
|
||||
href: `${layoutPath}/nebula`,
|
||||
label: "Nebula",
|
||||
icon: NebulaIcon,
|
||||
tracking: tracking("nebula"),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
href: `${layoutPath}/insight`,
|
||||
label: "Insight",
|
||||
|
||||
+3
-12
@@ -1,31 +1,22 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { BadgeContainer, mobileViewport } from "stories/utils";
|
||||
import { BadgeContainer } from "stories/utils";
|
||||
import type { RpcMethodStats } from "types/analytics";
|
||||
import { RpcMethodBarChartCardUI } from "./RpcMethodBarChartCardUI";
|
||||
|
||||
const meta = {
|
||||
title: "Analytics/RpcMethodBarChartCard",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
} satisfies Meta<typeof Component>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
parameters: {
|
||||
viewport: { defaultViewport: "desktop" },
|
||||
},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
const generateTimeSeriesData = (
|
||||
days: number,
|
||||
methods: string[],
|
||||
@@ -74,7 +65,7 @@ const commonMethods = [
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div className="container space-y-8 py-8">
|
||||
<div className="container max-w-6xl space-y-10 py-10">
|
||||
<BadgeContainer label="Lot of RPC methods - show 10 at max, combines rest in 'Other'">
|
||||
<RpcMethodBarChartCardUI
|
||||
rawData={generateTimeSeriesData(30, commonMethods)}
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TabButtons } from "@/components/ui/tabs";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
export function WaitingForIntegrationCard(props: {
|
||||
title: string;
|
||||
codeTabs: {
|
||||
code: React.ReactNode;
|
||||
label: string;
|
||||
}[];
|
||||
ctas: {
|
||||
label: string;
|
||||
href: string;
|
||||
trackingLabel: string;
|
||||
category: string;
|
||||
}[];
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
const [selectedTab, setSelectedTab] = useState(props.codeTabs[0]?.label);
|
||||
return (
|
||||
<div className="rounded-lg border bg-card">
|
||||
<div className="border-b px-4 py-4 lg:px-6">
|
||||
<h2 className="font-semibold text-xl tracking-tight">{props.title}</h2>
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-6 lg:p-6">
|
||||
{props.children}
|
||||
{/* Code */}
|
||||
<div>
|
||||
<TabButtons
|
||||
tabClassName="!text-sm"
|
||||
tabs={props.codeTabs.map((tab) => ({
|
||||
name: tab.label,
|
||||
onClick: () => setSelectedTab(tab.label),
|
||||
isActive: tab.label === selectedTab,
|
||||
}))}
|
||||
/>
|
||||
<div className="h-2" />
|
||||
{props.codeTabs.find((tab) => tab.label === selectedTab)?.code}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 border-t p-4 lg:flex-row lg:items-center lg:justify-between lg:p-6">
|
||||
<div className="flex gap-3">
|
||||
{props.ctas.map((cta) => (
|
||||
<Button asChild key={cta.label} variant="outline" size="sm">
|
||||
<TrackedLinkTW
|
||||
href={cta.href}
|
||||
key={cta.label}
|
||||
target="_blank"
|
||||
className="gap-2"
|
||||
category={cta.category}
|
||||
label={cta.trackingLabel}
|
||||
>
|
||||
{cta.label}
|
||||
<ExternalLinkIcon className="size-4 text-muted-foreground" />
|
||||
</TrackedLinkTW>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="flex items-center gap-2 rounded-full border bg-background px-3.5 py-1.5 text-sm">
|
||||
<span className="!pointer-events-auto relative flex size-2">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-sky-400 opacity-75" />
|
||||
<span className="relative inline-flex size-2 rounded-full bg-primary" />
|
||||
</span>
|
||||
Waiting for integration
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { LinkIcon } from "lucide-react";
|
||||
|
||||
type FooterSectionProps = {
|
||||
title: string;
|
||||
links: {
|
||||
href: string;
|
||||
label: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
type FooterCardProps = {
|
||||
left: FooterSectionProps;
|
||||
center: FooterSectionProps;
|
||||
right: FooterSectionProps;
|
||||
trackingCategory: string;
|
||||
};
|
||||
|
||||
export function FooterLinksSection(props: FooterCardProps) {
|
||||
return (
|
||||
<div className="py-6">
|
||||
<div className="grid grid-cols-1 gap-6 xl:grid-cols-3 xl:gap-0 xl:divide-x xl:divide-border">
|
||||
<div className="xl:pr-6">
|
||||
<FooterSection
|
||||
{...props.left}
|
||||
trackingCategory={props.trackingCategory}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="xl:px-6">
|
||||
<FooterSection
|
||||
{...props.center}
|
||||
trackingCategory={props.trackingCategory}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="xl:pl-6">
|
||||
<FooterSection
|
||||
{...props.right}
|
||||
trackingCategory={props.trackingCategory}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FooterSection(
|
||||
props: FooterSectionProps & { trackingCategory: string },
|
||||
) {
|
||||
return (
|
||||
<div>
|
||||
<h3 className="mb-2.5 font-medium">{props.title}</h3>
|
||||
<div className="flex flex-col gap-2.5">
|
||||
{props.links.map((link) => (
|
||||
<FooterLink
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
trackingCategory={props.trackingCategory}
|
||||
label={link.label}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FooterLink({
|
||||
href,
|
||||
label,
|
||||
trackingCategory,
|
||||
}: {
|
||||
href: string;
|
||||
label: string;
|
||||
trackingCategory: string;
|
||||
}) {
|
||||
return (
|
||||
<TrackedLinkTW
|
||||
href={href}
|
||||
category={trackingCategory}
|
||||
label={label.replace(/ /g, "-").toLowerCase()}
|
||||
target="_blank"
|
||||
className="flex items-start gap-2 text-balance text-muted-foreground text-sm hover:text-foreground"
|
||||
>
|
||||
<LinkIcon className="mt-1 size-3 shrink-0 opacity-70" />
|
||||
{label}
|
||||
</TrackedLinkTW>
|
||||
);
|
||||
}
|
||||
+72
-104
@@ -1,108 +1,76 @@
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { FooterLinksSection } from "../../components/footer/FooterLinksSection";
|
||||
|
||||
type LinkInfo = { label: string; href: string; trackingLabel: string };
|
||||
|
||||
export function AAFooterSection(props: {
|
||||
trackingCategory: string;
|
||||
}) {
|
||||
export function AAFooter() {
|
||||
return (
|
||||
<section className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||
<FooterCard
|
||||
title="Docs"
|
||||
links={docLinks}
|
||||
trackingCategory={props.trackingCategory}
|
||||
/>
|
||||
|
||||
<FooterCard
|
||||
title="Guides"
|
||||
links={guideLinks}
|
||||
trackingCategory={props.trackingCategory}
|
||||
/>
|
||||
|
||||
<FooterCard
|
||||
title="Templates"
|
||||
links={templateLinks}
|
||||
trackingCategory={props.trackingCategory}
|
||||
/>
|
||||
</section>
|
||||
<FooterLinksSection
|
||||
trackingCategory="account-abstraction"
|
||||
left={{
|
||||
title: "Documentation",
|
||||
links: [
|
||||
{
|
||||
href: "https://portal.thirdweb.com/connect/account-abstraction/overview",
|
||||
label: "Overview",
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/typescript/v5/account-abstraction/get-started",
|
||||
label: "Typescript",
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/react/v5/account-abstraction/get-started",
|
||||
label: "React",
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/react/v5/account-abstraction/get-started",
|
||||
label: "React Native",
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/dotnet/wallets/providers/account-abstraction",
|
||||
label: ".NET",
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/unity/v5/wallets/account-abstraction",
|
||||
label: "Unity",
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/unreal-engine/blueprints/smart-wallet",
|
||||
label: "Unreal Engine",
|
||||
},
|
||||
],
|
||||
}}
|
||||
right={{
|
||||
title: "Tutorials",
|
||||
links: [
|
||||
{
|
||||
label: "Web3 onboarding with in-app wallets and AA",
|
||||
href: "https://www.youtube.com/watch?v=HP8wSH7gnQA",
|
||||
},
|
||||
{
|
||||
label: "Multi-chain experience with account abstraction",
|
||||
href: "https://www.youtube.com/watch?v=2wUtq-drxAk",
|
||||
},
|
||||
{
|
||||
label: "Add AA to a web3 unity game",
|
||||
href: "https://www.youtube.com/watch?v=04G4DLhqwgI",
|
||||
},
|
||||
],
|
||||
}}
|
||||
center={{
|
||||
title: "Demos",
|
||||
links: [
|
||||
{
|
||||
label: "Connect Smart Accounts",
|
||||
href: "https://playground.thirdweb.com/connect/account-abstraction/connect",
|
||||
},
|
||||
{
|
||||
label: "Sponsor Gas Fees",
|
||||
href: "https://playground.thirdweb.com/connect/account-abstraction/sponsor",
|
||||
},
|
||||
{
|
||||
label: "Native Account Abstraction",
|
||||
href: "https://playground.thirdweb.com/connect/account-abstraction/native-aa",
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FooterCard(props: {
|
||||
title: string;
|
||||
links: LinkInfo[];
|
||||
trackingCategory: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-lg border border-border bg-card p-4 lg:p-6">
|
||||
<h3 className="mb-5 font-semibold text-lg tracking-tight lg:text-xl">
|
||||
{props.title}
|
||||
</h3>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{props.links.map((link) => (
|
||||
<TrackedLinkTW
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
category={props.trackingCategory}
|
||||
label={link.trackingLabel}
|
||||
target="_blank"
|
||||
className="text-muted-foreground text-sm hover:text-foreground"
|
||||
>
|
||||
{link.label}
|
||||
</TrackedLinkTW>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const docLinks: LinkInfo[] = [
|
||||
{
|
||||
label: "Full Docs",
|
||||
href: "https://portal.thirdweb.com/wallets/smart-wallet",
|
||||
trackingLabel: "full-docs",
|
||||
},
|
||||
{
|
||||
label: "Account Abstraction with React SDK",
|
||||
href: "https://portal.thirdweb.com/wallets/smart-wallet/guides/react",
|
||||
trackingLabel: "smart-wallet-react",
|
||||
},
|
||||
{
|
||||
label: "Account Abstraction with TypeScript SDK",
|
||||
href: "https://portal.thirdweb.com/wallets/smart-wallet/guides/typescript",
|
||||
trackingLabel: "smart-wallet-typescript",
|
||||
},
|
||||
];
|
||||
|
||||
const guideLinks: LinkInfo[] = [
|
||||
{
|
||||
label: "Deploy a Smart Account (ERC-4337)",
|
||||
href: "https://blog.thirdweb.com/guides/how-to-use-erc4337-smart-wallets/",
|
||||
trackingLabel: "deploy-smart-wallet",
|
||||
},
|
||||
{
|
||||
label:
|
||||
"How to Extend the Base Account Abstraction Contracts Using the Solidity SDK",
|
||||
href: "https://blog.thirdweb.com/guides/custom-smart-wallet-contracts/",
|
||||
trackingLabel: "extend-base-smart-wallet",
|
||||
},
|
||||
{
|
||||
label: "Batch Transactions with Account Abstraction",
|
||||
href: "https://blog.thirdweb.com/guides/how-to-batch-transactions-with-the-thirdweb-sdk/",
|
||||
trackingLabel: "batch-txns",
|
||||
},
|
||||
];
|
||||
|
||||
const templateLinks: LinkInfo[] = [
|
||||
{
|
||||
label: "Node.js template",
|
||||
trackingLabel: "node-template",
|
||||
href: "https://github.com/thirdweb-example/smart-wallet-script",
|
||||
},
|
||||
{
|
||||
label: "React template",
|
||||
trackingLabel: "react-template",
|
||||
href: "https://github.com/thirdweb-example/smart-wallet-react",
|
||||
},
|
||||
];
|
||||
|
||||
+125
-76
@@ -1,100 +1,149 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TabPathLinks } from "@/components/ui/tabs";
|
||||
import { TrackedUnderlineLink } from "@/components/ui/tracked-link";
|
||||
import {
|
||||
TrackedLinkTW,
|
||||
TrackedUnderlineLink,
|
||||
} from "@/components/ui/tracked-link";
|
||||
import { SmartWalletsBillingAlert } from "components/settings/ApiKeys/Alerts";
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { useActiveWalletChain } from "thirdweb/react";
|
||||
import type { UserOpStats } from "types/analytics";
|
||||
import { AccountAbstractionSummary } from "../../../../../../components/smart-wallets/AccountAbstractionAnalytics/AccountAbstractionSummary";
|
||||
import { AAFooterSection } from "./AAFooterSection";
|
||||
import { isOpChainId } from "./isOpChain";
|
||||
import { useLocalStorage } from "hooks/useLocalStorage";
|
||||
import { ArrowRightIcon } from "lucide-react";
|
||||
import { XIcon } from "lucide-react";
|
||||
import { AAFooter } from "./AAFooterSection";
|
||||
|
||||
const TRACKING_CATEGORY = "smart-wallet";
|
||||
|
||||
export function AccountAbstractionLayout(props: {
|
||||
projectSlug: string;
|
||||
projectId: string;
|
||||
teamSlug: string;
|
||||
projectKey: string;
|
||||
children: React.ReactNode;
|
||||
hasSmartWalletsWithoutBilling: boolean;
|
||||
userOpStats: UserOpStats;
|
||||
}) {
|
||||
const chain = useActiveWalletChain();
|
||||
|
||||
const isOpChain = chain?.id ? isOpChainId(chain.id) : false;
|
||||
|
||||
const smartWalletsLayoutSlug = `/team/${props.teamSlug}/${props.projectSlug}/connect/account-abstraction`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="mb-1 font-semibold text-2xl tracking-tight lg:mb-2 lg:text-3xl">
|
||||
Account Abstraction
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Easily integrate Account abstraction (ERC-4337) compliant smart accounts
|
||||
into your apps.{" "}
|
||||
<TrackedUnderlineLink
|
||||
target="_blank"
|
||||
label="docs-wallets"
|
||||
category={TRACKING_CATEGORY}
|
||||
href="https://portal.thirdweb.com/wallets/smart-wallet"
|
||||
>
|
||||
View Documentation
|
||||
</TrackedUnderlineLink>
|
||||
</p>
|
||||
<div className="flex grow flex-col">
|
||||
{/* top */}
|
||||
<div className="pt-4 lg:pt-6">
|
||||
{/* header */}
|
||||
<div className="container flex max-w-7xl flex-col gap-4">
|
||||
<div>
|
||||
<h1 className="mb-1 font-semibold text-2xl tracking-tight lg:text-3xl">
|
||||
Account Abstraction
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Integrate ERC-4337 compliant smart accounts for gasless
|
||||
sponsorships and more. <br className="lg:hidden" />
|
||||
<TrackedUnderlineLink
|
||||
target="_blank"
|
||||
label="docs-wallets"
|
||||
category={TRACKING_CATEGORY}
|
||||
href="https://portal.thirdweb.com/wallets/smart-wallet"
|
||||
>
|
||||
Learn more about Account Abstraction
|
||||
</TrackedUnderlineLink>
|
||||
</p>
|
||||
</div>
|
||||
{props.hasSmartWalletsWithoutBilling && <SmartWalletsBillingAlert />}
|
||||
<GasCreditAlert
|
||||
teamSlug={props.teamSlug}
|
||||
projectId={props.projectId}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-4" />
|
||||
|
||||
{/* Nav */}
|
||||
<TabPathLinks
|
||||
scrollableClassName="container max-w-7xl"
|
||||
links={[
|
||||
{
|
||||
name: "Analytics",
|
||||
path: `${smartWalletsLayoutSlug}`,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
name: "Account Factories",
|
||||
path: `${smartWalletsLayoutSlug}/factories`,
|
||||
},
|
||||
{
|
||||
name: "Sponsorship Rules",
|
||||
path: `${smartWalletsLayoutSlug}/settings`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* content */}
|
||||
<div className="h-6" />
|
||||
<div className="flex flex-col gap-6">
|
||||
{props.hasSmartWalletsWithoutBilling ? (
|
||||
<SmartWalletsBillingAlert />
|
||||
) : (
|
||||
isOpChain && (
|
||||
<Alert variant="info">
|
||||
<CircleAlertIcon className="size-4" />
|
||||
<AlertTitle>
|
||||
Using the gas credits for OP chain paymaster
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
Credits will automatically be applied to cover gas fees for any
|
||||
onchain activity across thirdweb services. <br />
|
||||
Eligible chains: OP Mainnet, Base, Zora, Frax, Mode.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)
|
||||
)}
|
||||
<div className="container flex max-w-7xl grow flex-col gap-6">
|
||||
<div>{props.children}</div>
|
||||
</div>
|
||||
<div className="h-20" />
|
||||
|
||||
<div>
|
||||
<AccountAbstractionSummary
|
||||
aggregateUserOpUsageQuery={props.userOpStats}
|
||||
/>
|
||||
|
||||
<div className="h-12" />
|
||||
<TabPathLinks
|
||||
links={[
|
||||
{
|
||||
name: "Analytics",
|
||||
path: `${smartWalletsLayoutSlug}`,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
name: "Account Factories",
|
||||
path: `${smartWalletsLayoutSlug}/factories`,
|
||||
},
|
||||
{
|
||||
name: "Sponsorship Rules",
|
||||
path: `${smartWalletsLayoutSlug}/settings`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
{props.children}
|
||||
{/* footer */}
|
||||
<div className="border-border border-t">
|
||||
<div className="container max-w-7xl">
|
||||
<AAFooter />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GasCreditAlert(props: {
|
||||
teamSlug: string;
|
||||
projectId: string;
|
||||
}) {
|
||||
const [isVisible, setIsVisible] = useLocalStorage(
|
||||
`gas-credit-${props.projectId}`,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<div className="relative rounded-lg border border-border bg-card p-4">
|
||||
<Button
|
||||
onClick={() => setIsVisible(false)}
|
||||
className="absolute top-4 right-4 h-auto w-auto p-1 text-muted-foreground"
|
||||
aria-label="Close alert"
|
||||
variant="ghost"
|
||||
>
|
||||
<XIcon className="size-5" />
|
||||
</Button>
|
||||
<div>
|
||||
<h2 className="mb-0.5 font-semibold">OP Gas Credit Program</h2>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Redeem credits towards gas Sponsorship. <br className="lg:hidden" />
|
||||
<TrackedUnderlineLink
|
||||
target="_blank"
|
||||
label="superchain-landing"
|
||||
category={TRACKING_CATEGORY}
|
||||
href="https://thirdweb.com/grant/superchain"
|
||||
>
|
||||
Learn More
|
||||
</TrackedUnderlineLink>
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex items-center gap-4">
|
||||
<Button asChild variant="outline" size="sm" className="bg-background">
|
||||
<TrackedLinkTW
|
||||
href={`/team/${props.teamSlug}/~/settings/credits`}
|
||||
target="_blank"
|
||||
className="gap-2"
|
||||
category={TRACKING_CATEGORY}
|
||||
label="claim-credits"
|
||||
>
|
||||
Claim your credits <ArrowRightIcon className="size-4" />
|
||||
</TrackedLinkTW>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-14" />
|
||||
<AAFooterSection trackingCategory={TRACKING_CATEGORY} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
export function isOpChainId(chainId: number) {
|
||||
return [10, 8453, 7777777, 252, 34443].includes(chainId);
|
||||
}
|
||||
+1
-7
@@ -1,4 +1,3 @@
|
||||
import { getAggregateUserOpUsage } from "@/api/analytics";
|
||||
import { getProject } from "@/api/projects";
|
||||
import { getTeamBySlug } from "@/api/team";
|
||||
import type { Metadata } from "next";
|
||||
@@ -34,18 +33,13 @@ export default async function Page(props: {
|
||||
team.billingStatus !== "validPayment" &&
|
||||
team.billingStatus !== "pastDue";
|
||||
|
||||
const userOpStats = await getAggregateUserOpUsage({
|
||||
teamId: team.id,
|
||||
projectId: project.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<AccountAbstractionLayout
|
||||
userOpStats={userOpStats}
|
||||
projectSlug={project.slug}
|
||||
teamSlug={team_slug}
|
||||
projectKey={project.publishableKey}
|
||||
hasSmartWalletsWithoutBilling={hasSmartWalletsWithoutBilling}
|
||||
projectId={project.id}
|
||||
>
|
||||
{props.children}
|
||||
</AccountAbstractionLayout>
|
||||
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
+25
-8
@@ -1,5 +1,6 @@
|
||||
import { getUserOpUsage } from "@/api/analytics";
|
||||
import { getProject } from "@/api/projects";
|
||||
import { getTeamBySlug } from "@/api/team";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import {
|
||||
type Range,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
import { AccountAbstractionAnalytics } from "components/smart-wallets/AccountAbstractionAnalytics";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import type { SearchParams } from "nuqs/server";
|
||||
import { AccountAbstractionSummary } from "../../../../../../components/smart-wallets/AccountAbstractionAnalytics/AccountAbstractionSummary";
|
||||
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
|
||||
import { searchParamLoader } from "./search-params";
|
||||
|
||||
@@ -31,7 +33,14 @@ export default async function Page(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const project = await getProject(params.team_slug, params.project_slug);
|
||||
const [team, project] = await Promise.all([
|
||||
getTeamBySlug(params.team_slug),
|
||||
getProject(params.team_slug, params.project_slug),
|
||||
]);
|
||||
|
||||
if (!team) {
|
||||
redirect("/team");
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
redirect(`/team/${params.team_slug}`);
|
||||
@@ -61,12 +70,20 @@ export default async function Page(props: {
|
||||
});
|
||||
|
||||
return (
|
||||
<AccountAbstractionAnalytics
|
||||
userOpStats={userOpStats}
|
||||
client={getThirdwebClient(authToken)}
|
||||
teamId={project.teamId}
|
||||
projectId={project.id}
|
||||
teamSlug={params.team_slug}
|
||||
/>
|
||||
<div>
|
||||
<AccountAbstractionSummary
|
||||
teamId={project.teamId}
|
||||
projectId={project.id}
|
||||
/>
|
||||
|
||||
<div className="h-10" />
|
||||
<AccountAbstractionAnalytics
|
||||
userOpStats={userOpStats}
|
||||
client={getThirdwebClient(authToken)}
|
||||
teamId={project.teamId}
|
||||
projectId={project.id}
|
||||
teamSlug={params.team_slug}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+75
-171
@@ -1,176 +1,80 @@
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { ArrowRightIcon } from "lucide-react";
|
||||
import { SupportedPlatformLink } from "../../../../../../../components/wallets/SupportedPlatformLink";
|
||||
import { FooterLinksSection } from "../../../components/footer/FooterLinksSection";
|
||||
|
||||
export function InAppWalletFooterSection(props: {
|
||||
trackingCategory: string;
|
||||
}) {
|
||||
export function InAppWalletsFooter() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
|
||||
<ViewDocs trackingCategory={props.trackingCategory} />
|
||||
<Templates trackingCategory={props.trackingCategory} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ViewDocs(props: {
|
||||
trackingCategory: string;
|
||||
}) {
|
||||
const TRACKING_CATEGORY = props.trackingCategory;
|
||||
return (
|
||||
<div className="rounded-lg border border-border bg-card p-4 lg:p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold">View Docs</h3>
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</div>
|
||||
|
||||
<div className="h-4" />
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<SupportedPlatformLink
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
platform="React"
|
||||
href="https://portal.thirdweb.com/connect/in-app-wallet/overview"
|
||||
/>
|
||||
|
||||
<SupportedPlatformLink
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
platform="Unity"
|
||||
href="https://portal.thirdweb.com/unity/wallets/providers/embedded-wallet"
|
||||
/>
|
||||
<SupportedPlatformLink
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
platform="React Native"
|
||||
href="https://portal.thirdweb.com/react/v5/in-app-wallet/get-started"
|
||||
/>
|
||||
<SupportedPlatformLink
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
platform="TypeScript"
|
||||
href="https://portal.thirdweb.com/connect/in-app-wallet/overview"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold">Relevant Guides</h3>
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</div>
|
||||
|
||||
<div className="h-4" />
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<GuideLink
|
||||
href="https://blog.thirdweb.com/what-are-embedded-wallets/"
|
||||
label="what-is-an-embedded-wallet"
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
>
|
||||
What is an in-app wallet?
|
||||
</GuideLink>
|
||||
|
||||
<GuideLink
|
||||
href="https://portal.thirdweb.com/connect/in-app-wallet/get-started"
|
||||
label="sdks-get-started"
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
>
|
||||
Get started with In-App Wallets
|
||||
</GuideLink>
|
||||
|
||||
<GuideLink
|
||||
href="https://portal.thirdweb.com/connect/in-app-wallet/how-to/connect-users"
|
||||
label="how-to-connect-your-users"
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
>
|
||||
Using In-App Wallets with Connect
|
||||
</GuideLink>
|
||||
|
||||
<GuideLink
|
||||
href="https://portal.thirdweb.com/connect/in-app-wallet/how-to/build-your-own-ui"
|
||||
label="how-to-build-your-own-ui"
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
>
|
||||
How to Build Your Own UI
|
||||
</GuideLink>
|
||||
|
||||
<GuideLink
|
||||
href="https://portal.thirdweb.com/connect/in-app-wallet/custom-auth/custom-auth-server"
|
||||
label="how-to-custom-auth-server"
|
||||
trackingCategory={TRACKING_CATEGORY}
|
||||
>
|
||||
Create a custom auth server
|
||||
</GuideLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GuideLink(props: {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
href: string;
|
||||
trackingCategory: string;
|
||||
}) {
|
||||
return (
|
||||
<TrackedLinkTW
|
||||
category={props.trackingCategory}
|
||||
label="guide"
|
||||
trackingProps={{
|
||||
guide: props.label,
|
||||
<FooterLinksSection
|
||||
trackingCategory="in-app-wallets"
|
||||
left={{
|
||||
title: "Documentation",
|
||||
links: [
|
||||
{
|
||||
href: "https://portal.thirdweb.com/connect/wallet/overview",
|
||||
label: "Overview",
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/typescript/v5/in-app-wallet",
|
||||
label: "Typescript",
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/react/v5/in-app-wallet/get-started",
|
||||
label: "React",
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/react/v5/in-app-wallet/get-started",
|
||||
label: "React Native",
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/dotnet/wallets/providers/in-app-wallet",
|
||||
label: ".NET",
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/unity/v5/wallets/in-app-wallet",
|
||||
label: "Unity",
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/unreal-engine/blueprints/in-app-wallet",
|
||||
label: "Unreal Engine",
|
||||
},
|
||||
],
|
||||
}}
|
||||
href={props.href}
|
||||
className="!text-muted-foreground hover:!text-foreground text-sm"
|
||||
target="_blank"
|
||||
>
|
||||
{props.children}
|
||||
</TrackedLinkTW>
|
||||
);
|
||||
}
|
||||
|
||||
function Templates(props: {
|
||||
trackingCategory: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-lg border border-border bg-card p-4 lg:p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="font-semibold">Relevant Templates</h3>
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</div>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<GuideLink
|
||||
href="https://github.com/thirdweb-example/embedded-smart-wallet"
|
||||
label="embedded-smart-wallet"
|
||||
trackingCategory={props.trackingCategory}
|
||||
>
|
||||
In-App Wallet + Account Abstraction Starter Kit
|
||||
</GuideLink>
|
||||
|
||||
<GuideLink
|
||||
href="https://github.com/thirdweb-example/catattacknft"
|
||||
label="embedded-cat-attack"
|
||||
trackingCategory={props.trackingCategory}
|
||||
>
|
||||
Cat Attack [Demo Web Game]
|
||||
</GuideLink>
|
||||
|
||||
<GuideLink
|
||||
href="https://github.com/thirdweb-example/embedded-wallet-custom-ui"
|
||||
label="embedded-wallet-with-custom-ui-react"
|
||||
trackingCategory={props.trackingCategory}
|
||||
>
|
||||
In-App Wallet With Custom UI [React]
|
||||
</GuideLink>
|
||||
|
||||
<GuideLink
|
||||
href="https://github.com/thirdweb-example/embedded-wallet-custom-ui-react-native"
|
||||
label="embedded-wallet-with-custom-ui-react-native"
|
||||
trackingCategory={props.trackingCategory}
|
||||
>
|
||||
In-App Wallet With Custom UI [ReactNative]
|
||||
</GuideLink>
|
||||
</div>
|
||||
</div>
|
||||
center={{
|
||||
title: "Tutorials",
|
||||
links: [
|
||||
{
|
||||
href: "https://www.youtube.com/watch?v=9s3Z-MvPv2I",
|
||||
label: "Build your own custom connect component",
|
||||
},
|
||||
{
|
||||
label: "Learn to create wallets inside a Unity game",
|
||||
href: "https://www.youtube.com/watch?v=oUouNueoVFU",
|
||||
},
|
||||
{
|
||||
href: "https://www.youtube.com/watch?v=xEp-fKba_cI",
|
||||
label: "Pre-generate wallets for users",
|
||||
},
|
||||
{
|
||||
label: "Sign-in with guest mode",
|
||||
href: "https://portal.thirdweb.com/connect/wallet/sign-in-methods/guest",
|
||||
},
|
||||
],
|
||||
}}
|
||||
right={{
|
||||
title: "Demos",
|
||||
links: [
|
||||
{
|
||||
href: "https://playground.thirdweb.com/connect/in-app-wallet/ecosystem",
|
||||
label: "Ecosystems",
|
||||
},
|
||||
{
|
||||
href: "https://playground.thirdweb.com/connect/in-app-wallet/sponsor",
|
||||
label: "Signless Sponsored Transactions",
|
||||
},
|
||||
{
|
||||
href: "https://playground.thirdweb.com/connect/in-app-wallet",
|
||||
label: "Customized UI",
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+6
-37
@@ -1,43 +1,16 @@
|
||||
import { getInAppWalletUsage } from "@/api/analytics";
|
||||
import { TrackedUnderlineLink } from "@/components/ui/tracked-link";
|
||||
import { InAppWalletsSummary } from "components/embedded-wallets/Analytics/Summary";
|
||||
import { subDays } from "date-fns";
|
||||
import { TRACKING_CATEGORY } from "../_constants";
|
||||
|
||||
export async function InAppWalletsHeader({
|
||||
teamId,
|
||||
projectId,
|
||||
}: { teamId: string; projectId: string }) {
|
||||
const allTimeStatsPromise = getInAppWalletUsage({
|
||||
teamId,
|
||||
projectId,
|
||||
from: new Date(2022, 0, 1),
|
||||
to: new Date(),
|
||||
period: "all",
|
||||
});
|
||||
|
||||
const monthlyStatsPromise = getInAppWalletUsage({
|
||||
teamId,
|
||||
projectId,
|
||||
from: subDays(new Date(), 30),
|
||||
to: new Date(),
|
||||
period: "month",
|
||||
});
|
||||
|
||||
const [allTimeStats, monthlyStats] = await Promise.all([
|
||||
allTimeStatsPromise,
|
||||
monthlyStatsPromise,
|
||||
]).catch(() => [null, null]);
|
||||
|
||||
export async function InAppWalletsHeader() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="mb-1 font-semibold text-2xl tracking-tight lg:mb-2 lg:text-3xl">
|
||||
<div className="">
|
||||
<h1 className="mb-2 font-semibold text-2xl tracking-tighter lg:text-3xl">
|
||||
In-App Wallets
|
||||
</h1>
|
||||
<p className="mb-7 max-w-[700px] text-muted-foreground text-sm leading-relaxed">
|
||||
<p className="max-w-2xl text-muted-foreground text-sm leading-relaxed">
|
||||
A wallet infrastructure that enables apps to create, manage, and control
|
||||
their users wallets. Email login, social login, and bring-your-own auth
|
||||
supported.{" "}
|
||||
their users wallets. <br /> Email login, social login, and
|
||||
bring-your-own auth supported.{" "}
|
||||
<TrackedUnderlineLink
|
||||
target="_blank"
|
||||
href="https://portal.thirdweb.com/connect/in-app-wallet/overview"
|
||||
@@ -47,10 +20,6 @@ export async function InAppWalletsHeader({
|
||||
Learn more
|
||||
</TrackedUnderlineLink>
|
||||
</p>
|
||||
<InAppWalletsSummary
|
||||
allTimeStats={allTimeStats || []}
|
||||
monthlyStats={monthlyStats || []}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+41
-28
@@ -1,9 +1,8 @@
|
||||
import { getProject } from "@/api/projects";
|
||||
import { TabPathLinks } from "@/components/ui/tabs";
|
||||
import { redirect } from "next/navigation";
|
||||
import { InAppWalletFooterSection } from "./_components/footer";
|
||||
import { InAppWalletsFooter } from "./_components/footer";
|
||||
import { InAppWalletsHeader } from "./_components/header";
|
||||
import { TRACKING_CATEGORY } from "./_constants";
|
||||
|
||||
export default async function Layout(props: {
|
||||
params: Promise<{
|
||||
@@ -22,34 +21,48 @@ export default async function Layout(props: {
|
||||
const { team_slug, project_slug } = params;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InAppWalletsHeader teamId={project.teamId} projectId={project.id} />
|
||||
<div className="h-8" />
|
||||
<div className="flex grow flex-col">
|
||||
{/* header */}
|
||||
<div className="pt-4 lg:pt-6">
|
||||
<div className="container max-w-7xl">
|
||||
<InAppWalletsHeader />
|
||||
</div>
|
||||
<div className="h-3" />
|
||||
<TabPathLinks
|
||||
scrollableClassName="container max-w-7xl"
|
||||
links={[
|
||||
{
|
||||
name: "Analytics",
|
||||
path: `/team/${team_slug}/${project_slug}/connect/in-app-wallets`,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
name: "Users",
|
||||
path: `/team/${team_slug}/${project_slug}/connect/in-app-wallets/users`,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
path: `/team/${team_slug}/${project_slug}/connect/in-app-wallets/settings`,
|
||||
exactMatch: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TabPathLinks
|
||||
links={[
|
||||
{
|
||||
name: "Analytics",
|
||||
path: `/team/${team_slug}/${project_slug}/connect/in-app-wallets`,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
name: "Users",
|
||||
path: `/team/${team_slug}/${project_slug}/connect/in-app-wallets/users`,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
path: `/team/${team_slug}/${project_slug}/connect/in-app-wallets/settings`,
|
||||
exactMatch: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{/* content */}
|
||||
<div className="container flex max-w-7xl grow flex-col">
|
||||
<div className="h-6" />
|
||||
{props.children}
|
||||
<div className="h-20" />
|
||||
</div>
|
||||
|
||||
<div className="h-8" />
|
||||
{props.children}
|
||||
<div className="h-8" />
|
||||
<InAppWalletFooterSection trackingCategory={TRACKING_CATEGORY} />
|
||||
{/* footer */}
|
||||
<div className="border-t">
|
||||
<div className="container max-w-7xl">
|
||||
<InAppWalletsFooter />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
+12
-7
@@ -1,7 +1,8 @@
|
||||
import { getProject } from "@/api/projects";
|
||||
import type { Range } from "components/analytics/date-range-selector";
|
||||
import { InAppWalletAnalytics } from "components/embedded-wallets/Analytics";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getProject } from "../../../../../../@/api/projects";
|
||||
import { InAppWalletsSummary } from "../../../../../../components/embedded-wallets/Analytics/Summary";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ team_slug: string; project_slug: string }>;
|
||||
@@ -39,11 +40,15 @@ export default async function Page(props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<InAppWalletAnalytics
|
||||
teamId={project.teamId}
|
||||
projectId={project.id}
|
||||
interval={interval}
|
||||
range={range as Range}
|
||||
/>
|
||||
<div>
|
||||
<InAppWalletsSummary teamId={project.teamId} projectId={project.id} />
|
||||
<div className="h-10" />
|
||||
<InAppWalletAnalytics
|
||||
teamId={project.teamId}
|
||||
projectId={project.id}
|
||||
interval={interval}
|
||||
range={range as Range}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
team_slug: string;
|
||||
project_slug: string;
|
||||
}>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
|
||||
redirect(
|
||||
`/team/${params.team_slug}/${params.project_slug}/connect/in-app-wallets`,
|
||||
);
|
||||
}
|
||||
+96
-24
@@ -1,7 +1,8 @@
|
||||
import { getProject } from "@/api/projects";
|
||||
import { UnderlineLink } from "@/components/ui/UnderlineLink";
|
||||
import { TabPathLinks } from "@/components/ui/tabs";
|
||||
import { redirect } from "next/navigation";
|
||||
import { UnderlineLink } from "../../../../../../@/components/ui/UnderlineLink";
|
||||
import { FooterLinksSection } from "../../components/footer/FooterLinksSection";
|
||||
|
||||
export default async function Layout(props: {
|
||||
params: Promise<{
|
||||
@@ -20,13 +21,13 @@ export default async function Layout(props: {
|
||||
const payLayoutPath = `/team/${params.team_slug}/${params.project_slug}/connect/universal-bridge`;
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<div className="flex flex-col items-start justify-between gap-6 lg:flex-row">
|
||||
<div className="max-w-[700px]">
|
||||
<div className="flex grow flex-col">
|
||||
<div className="pt-4 lg:pt-6">
|
||||
<div className="container max-w-7xl">
|
||||
<h1 className="mb-1 font-semibold text-2xl tracking-tight lg:mb-2 lg:text-3xl">
|
||||
Universal Bridge
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||
<p className="max-w-3xl text-muted-foreground text-sm leading-relaxed">
|
||||
Universal Bridge allows your users to bridge, swap, and purchase
|
||||
cryptocurrencies and execute transactions with any fiat options or
|
||||
tokens via cross-chain routing.{" "}
|
||||
@@ -38,28 +39,99 @@ export default async function Layout(props: {
|
||||
</UnderlineLink>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="h-4" />
|
||||
<TabPathLinks
|
||||
scrollableClassName="container max-w-7xl"
|
||||
className="w-full"
|
||||
links={[
|
||||
{
|
||||
name: "Analytics",
|
||||
path: payLayoutPath,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
name: "Webhooks",
|
||||
path: `${payLayoutPath}/webhooks`,
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
path: `${payLayoutPath}/settings`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TabPathLinks
|
||||
className="w-full"
|
||||
links={[
|
||||
{
|
||||
name: "Analytics",
|
||||
path: payLayoutPath,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
name: "Webhooks",
|
||||
path: `${payLayoutPath}/webhooks`,
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
path: `${payLayoutPath}/settings`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="h-6" />
|
||||
<div className="container flex max-w-7xl grow flex-col">
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
{props.children}
|
||||
<div className="h-20" />
|
||||
<div className="border-border border-t">
|
||||
<div className="container max-w-7xl">
|
||||
<UBFooter />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UBFooter() {
|
||||
return (
|
||||
<FooterLinksSection
|
||||
trackingCategory="universal-bridge"
|
||||
left={{
|
||||
title: "Documentation",
|
||||
links: [
|
||||
{
|
||||
href: "https://portal.thirdweb.com/connect/pay/overview",
|
||||
label: "Overview",
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/typescript/v5/convertCryptoToFiat",
|
||||
label: "TypeScript",
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/react/v5/pay/fund-wallets",
|
||||
label: "React",
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com/dotnet/universal-bridge/quickstart",
|
||||
label: ".NET",
|
||||
},
|
||||
],
|
||||
}}
|
||||
right={{
|
||||
title: "Tutorials",
|
||||
links: [
|
||||
{
|
||||
label: "Implement cross-chain payments in any app",
|
||||
href: "https://www.youtube.com/watch?v=aBu175-VsNY",
|
||||
},
|
||||
],
|
||||
}}
|
||||
center={{
|
||||
title: "Demos",
|
||||
links: [
|
||||
{
|
||||
label: "UI Component",
|
||||
href: "https://playground.thirdweb.com/connect/pay",
|
||||
},
|
||||
{
|
||||
label: "Buy Crypto",
|
||||
href: "https://playground.thirdweb.com/connect/pay/fund-wallet",
|
||||
},
|
||||
{
|
||||
label: "Commerce",
|
||||
href: "https://playground.thirdweb.com/connect/pay/commerce",
|
||||
},
|
||||
{
|
||||
label: "Transactions",
|
||||
href: "https://playground.thirdweb.com/connect/pay/transactions",
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { redirect } from "next/navigation";
|
||||
import { DeployedContractsPage } from "../../../../account/contracts/_components/DeployedContractsPage";
|
||||
import { getAuthToken } from "../../../../api/lib/getAuthToken";
|
||||
import { loginRedirect } from "../../../../login/loginRedirect";
|
||||
import { FooterLinksSection } from "../components/footer/FooterLinksSection";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ team_slug: string; project_slug: string }>;
|
||||
@@ -29,10 +30,71 @@ export default async function Page(props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<DeployedContractsPage
|
||||
teamId={team.id}
|
||||
projectId={project.id}
|
||||
authToken={authToken}
|
||||
/>
|
||||
<div className="flex grow flex-col">
|
||||
<DeployedContractsPage
|
||||
teamId={team.id}
|
||||
projectId={project.id}
|
||||
authToken={authToken}
|
||||
/>
|
||||
<div className="h-20" />
|
||||
<ContractsFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ContractsFooter() {
|
||||
return (
|
||||
<div className="border-t">
|
||||
<div className="container max-w-7xl">
|
||||
<FooterLinksSection
|
||||
trackingCategory="contracts"
|
||||
left={{
|
||||
title: "Documentation",
|
||||
links: [
|
||||
{
|
||||
label: "Deployment Tools",
|
||||
href: "https://portal.thirdweb.com/contracts/deploy/overview",
|
||||
},
|
||||
{
|
||||
label: "Modular Contracts",
|
||||
href: "https://portal.thirdweb.com/contracts/modular-contracts/overview",
|
||||
},
|
||||
{
|
||||
label: "Pre-built Contracts",
|
||||
href: "https://portal.thirdweb.com/contracts/explore/overview",
|
||||
},
|
||||
],
|
||||
}}
|
||||
center={{
|
||||
title: "Templates",
|
||||
links: [
|
||||
{
|
||||
label: "Hardhat Starter",
|
||||
href: "https://thirdweb.com/templates/hardhat-starter",
|
||||
},
|
||||
{
|
||||
href: "https://thirdweb.com/templates/forge-starter",
|
||||
label: "Forge Starter",
|
||||
},
|
||||
],
|
||||
}}
|
||||
right={{
|
||||
title: "Tutorials",
|
||||
links: [
|
||||
{
|
||||
label:
|
||||
"Everything you need to know about upgradeable smart contracts",
|
||||
href: "https://www.youtube.com/watch?v=cZt-CkzxrNM",
|
||||
},
|
||||
{
|
||||
label:
|
||||
"Modular Contracts SDK: Build Core & Modules from Scratch (Advanced Guide)",
|
||||
href: "https://www.youtube.com/watch?v=ZoOk41y4f_k",
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Code2Icon,
|
||||
DatabaseIcon,
|
||||
ExternalLinkIcon,
|
||||
ZapIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export function BlueprintCard() {
|
||||
const features = [
|
||||
{
|
||||
icon: Code2Icon,
|
||||
title: "Easy-to-Use API",
|
||||
description: "RESTful endpoints for any application",
|
||||
},
|
||||
{
|
||||
icon: DatabaseIcon,
|
||||
title: "Managed Infrastructure",
|
||||
description:
|
||||
"No need to index blockchains yourself or manage infrastructure and RPC costs.",
|
||||
},
|
||||
{
|
||||
icon: ZapIcon,
|
||||
title: "Lightning-Fast Queries",
|
||||
description: "Access any transaction, event or token API data",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-card">
|
||||
{/* header */}
|
||||
<div className="border-b p-4 lg:px-6 lg:py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="font-semibold text-2xl tracking-tight">Blueprints</h2>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button asChild variant="outline" className="gap-2 bg-background">
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/insight/blueprints"
|
||||
target="_blank"
|
||||
>
|
||||
Docs <ExternalLinkIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-4 lg:p-6">
|
||||
<p className="mb-2 font-semibold text-xl leading-tight tracking-tight lg:mb-1">
|
||||
Simple endpoints for querying rich blockchain data
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
A blueprint is an API that provides access to on-chain data in a
|
||||
user-friendly format. <br /> No need for ABIs, decoding, RPC, or web3
|
||||
knowledge required to fetch blockchain data.
|
||||
</p>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
{/* Features */}
|
||||
<div className="flex flex-col gap-6">
|
||||
{features.map((feature) => (
|
||||
<div key={feature.title} className="flex items-start gap-3">
|
||||
<div className="rounded-full border p-2">
|
||||
<feature.icon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium">{feature.title}</h4>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Playground link */}
|
||||
<div className="border-t p-4 lg:p-6">
|
||||
<Button className="w-full gap-2" asChild>
|
||||
<Link href="https://playground.thirdweb.com/insight" target="_blank">
|
||||
Try Insight blueprints in the playground
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user