Playground: Revamp (#7768)
@@ -0,0 +1,21 @@
|
||||
export function TokenIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="presentation"
|
||||
className={props.className}
|
||||
>
|
||||
<path
|
||||
d="M15.9377 15.9377C19.3603 15.4795 22 12.548 22 9C22 5.13401 18.866 2 15 2C11.452 2 8.52049 4.63967 8.06227 8.06227M16 15C16 18.866 12.866 22 9 22C5.13401 22 2 18.866 2 15C2 11.134 5.13401 8 9 8C12.866 8 16 11.134 16 15Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export function WalletProductIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="presentation"
|
||||
className={props.className}
|
||||
>
|
||||
<path
|
||||
d="M16 7.99995V4.5006C16 3.66887 16 3.253 15.8248 2.99742C15.6717 2.77413 15.4346 2.62245 15.1678 2.57703C14.8623 2.52505 14.4847 2.69932 13.7295 3.04787L4.85901 7.14194C4.18551 7.45279 3.84875 7.60822 3.60211 7.84927C3.38406 8.06237 3.21762 8.3225 3.1155 8.60978C3 8.93474 3 9.30563 3 10.0474V14.9999M16.5 14.4999H16.51M3 11.2L3 17.7999C3 18.9201 3 19.4801 3.21799 19.9079C3.40973 20.2843 3.71569 20.5902 4.09202 20.782C4.51984 20.9999 5.07989 20.9999 6.2 20.9999H17.8C18.9201 20.9999 19.4802 21 19.908 20.782C20.2843 20.5902 20.5903 20.2843 20.782 19.9079C21 19.4801 21 18.9201 21 17.7999V11.2C21 10.0798 21 9.51979 20.782 9.09197C20.5903 8.71565 20.2843 8.40969 19.908 8.21794C19.4802 7.99995 18.9201 7.99995 17.8 7.99995L6.2 7.99995C5.0799 7.99995 4.51984 7.99995 4.09202 8.21794C3.7157 8.40968 3.40973 8.71564 3.21799 9.09197C3 9.51979 3 10.0798 3 11.2ZM17 14.4999C17 14.7761 16.7761 14.9999 16.5 14.9999C16.2239 14.9999 16 14.7761 16 14.4999C16 14.2238 16.2239 13.9999 16.5 13.9999C16.7761 13.9999 17 14.2238 17 14.4999Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
import {
|
||||
AtomIcon,
|
||||
BookTextIcon,
|
||||
BoxIcon,
|
||||
ChartNoAxesColumnIcon,
|
||||
@@ -51,7 +52,7 @@ export function TeamSidebarLayout(props: {
|
||||
},
|
||||
],
|
||||
subMenu: {
|
||||
icon: WalletCardsIcon,
|
||||
icon: AtomIcon,
|
||||
label: "Ecosystems",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,12 +4,10 @@ import {
|
||||
BellIcon,
|
||||
BookTextIcon,
|
||||
BoxIcon,
|
||||
CoinsIcon,
|
||||
HomeIcon,
|
||||
LockIcon,
|
||||
RssIcon,
|
||||
SettingsIcon,
|
||||
WalletIcon,
|
||||
} from "lucide-react";
|
||||
import { FullWidthSidebarLayout } from "@/components/blocks/full-width-sidebar-layout";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -17,6 +15,8 @@ import { ContractIcon } from "@/icons/ContractIcon";
|
||||
import { InsightIcon } from "@/icons/InsightIcon";
|
||||
import { PayIcon } from "@/icons/PayIcon";
|
||||
import { SmartAccountIcon } from "@/icons/SmartAccountIcon";
|
||||
import { TokenIcon } from "@/icons/TokenIcon";
|
||||
import { WalletProductIcon } from "@/icons/WalletProductIcon";
|
||||
|
||||
export function ProjectSidebarLayout(props: {
|
||||
layoutPath: string;
|
||||
@@ -48,7 +48,7 @@ export function ProjectSidebarLayout(props: {
|
||||
links: [
|
||||
{
|
||||
href: `${layoutPath}/wallets`,
|
||||
icon: WalletIcon,
|
||||
icon: WalletProductIcon,
|
||||
label: "Wallets",
|
||||
},
|
||||
{
|
||||
@@ -85,7 +85,7 @@ export function ProjectSidebarLayout(props: {
|
||||
},
|
||||
{
|
||||
href: `${layoutPath}/tokens`,
|
||||
icon: CoinsIcon,
|
||||
icon: TokenIcon,
|
||||
label: (
|
||||
<span className="flex items-center gap-2">
|
||||
Tokens <Badge>New</Badge>
|
||||
|
||||
@@ -113,17 +113,37 @@ const nextConfig = {
|
||||
},
|
||||
{
|
||||
source: "/wallets/account-abstraction/sponsor",
|
||||
destination: "/wallets/account-abstraction/eip-4337",
|
||||
destination: "/account-abstraction/eip-4337",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/wallets/account-abstraction/7702",
|
||||
destination: "/wallets/account-abstraction/eip-7702",
|
||||
destination: "/account-abstraction/eip-7702",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/wallets/account-abstraction/5792",
|
||||
destination: "/wallets/account-abstraction/eip-5792",
|
||||
destination: "/account-abstraction/eip-5792",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/wallets/account-abstraction/native-aa",
|
||||
destination: "/account-abstraction/native-aa",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/wallets/headless/token-components",
|
||||
destination: "/tokens/token-components",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/wallets/headless/nft-components",
|
||||
destination: "/tokens/nft-components",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/wallets/in-app-wallet/ecosystem",
|
||||
destination: "/wallets/ecosystem-wallet",
|
||||
permanent: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
"@abstract-foundation/agw-react": "^1.6.4",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,96 @@
|
||||
<svg width="510" height="510" viewBox="0 0 510 510" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M423.472 360.235L356.633 387.064V308.174L423.472 281.345V360.235Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M422.767 280.725L356.214 307.438L289.662 280.725L356.214 254.011L422.767 280.725Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M355.795 308.173V386.238L289.585 281.597L355.795 308.173Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M355.166 386.812L288.957 360.235V282.171L355.166 386.812Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M423.891 280.725V360.52L356.214 387.685L288.538 360.52V280.725L356.214 253.56L423.891 280.725Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M423.891 280.726L356.214 307.891" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M356.214 387.685V307.891" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M356.214 307.891L288.538 280.726" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M423.472 253.272L356.633 280.101V201.211L423.472 174.382V253.272Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M422.767 173.762L356.214 200.475L289.662 173.762L356.214 147.048L422.767 173.762Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M355.795 201.21V279.275L289.585 174.634L355.795 201.21Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M355.166 279.849L288.957 253.272V175.208L355.166 279.849Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M423.891 173.762V253.557L356.214 280.722L288.538 253.557V173.762L356.214 146.597L423.891 173.762Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M423.891 173.762L356.214 200.928" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M356.214 280.722V200.928" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M356.214 200.928L288.538 173.762" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M423.472 146.314L356.633 173.143V94.2525L423.472 67.4234V146.314Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M422.767 66.8043L356.214 93.5172L289.662 66.8043L356.214 40.0905L422.767 66.8043Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M355.795 94.2516V172.317L289.585 67.6754L355.795 94.2516Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M355.166 172.891L288.957 146.314V68.2496L355.166 172.891Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M423.891 66.8044V146.599L356.214 173.764L288.538 146.599V66.8044L356.214 39.6393L423.891 66.8044Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M423.891 66.8043L356.214 93.9694" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M356.214 173.764V93.9694" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M356.214 93.9694L288.538 66.8043" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M321.958 400.132L255.119 426.961V348.07L321.958 321.241V400.132Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M321.252 320.622L254.7 347.335L188.148 320.622L254.7 293.908L321.252 320.622Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M254.281 348.069V426.135L188.071 321.493L254.281 348.069Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M253.652 426.709L187.443 400.132V322.067L253.652 426.709Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M322.377 320.622V400.417L254.7 427.582L187.024 400.417V320.622L254.7 293.457L322.377 320.622Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M322.377 320.622L254.7 347.787" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M254.7 427.583V347.788" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M254.7 347.787L187.024 320.622" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M321.958 294.259L255.119 321.088V242.198L321.958 215.368V294.259Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M321.252 214.749L254.7 241.462L188.148 214.749L254.7 188.035L321.252 214.749Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M254.281 242.197V320.262L188.071 215.62L254.281 242.197Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M253.652 320.836L187.443 294.259V216.195L253.652 320.836Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M322.377 214.749V294.544L254.7 321.709L187.024 294.544V214.749L254.7 187.584L322.377 214.749Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M322.377 214.749L254.7 241.914" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M254.7 321.709V241.914" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M254.7 241.914L187.024 214.749" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M324.131 187.298L257.292 214.127V135.236L324.131 108.407V187.298Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M323.425 107.788L256.873 134.501L190.321 107.788L256.873 81.074L323.425 107.788Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M256.454 135.235V213.301L190.244 108.659L256.454 135.235Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M255.825 213.875L189.616 187.298V109.233L255.825 213.875Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M324.55 107.788V187.582L256.873 214.747L189.197 187.582V107.788L256.873 80.6228L324.55 107.788Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M324.55 107.788L256.873 134.953" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M256.873 214.748V134.953" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M256.873 134.953L189.197 107.788" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M220.507 442.377L153.668 469.206V390.315L220.507 363.486V442.377Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M219.802 362.867L153.25 389.58L86.6978 362.867L153.25 336.153L219.802 362.867Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M152.831 390.314V468.38L86.6206 363.738L152.831 390.314Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M152.202 468.954L85.9927 442.377V364.312L152.202 468.954Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M220.927 362.867V442.661L153.25 469.827L85.5737 442.661V362.867L153.25 335.702L220.927 362.867Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M220.926 362.867L153.25 390.032" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M153.25 469.827V390.032" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M153.25 390.032L85.5737 362.867" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M220.508 335.415L153.669 362.244V283.354L220.508 256.525V335.415Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M219.802 255.906L153.25 282.618L86.6978 255.906L153.25 229.192L219.802 255.906Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M152.831 283.353V361.418L86.6206 256.777L152.831 283.353Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M152.202 361.992L85.9927 335.415V257.351L152.202 361.992Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M220.927 255.906V335.7L153.25 362.865L85.5737 335.7V255.906L153.25 228.74L220.927 255.906Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M220.927 255.906L153.25 283.071" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M153.25 362.865V283.071" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M153.25 283.071L85.5737 255.906" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M220.508 228.456L153.669 255.285V176.394L220.508 149.565V228.456Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M219.802 148.946L153.25 175.659L86.6978 148.946L153.25 122.232L219.802 148.946Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M152.831 176.393V254.458L86.6206 149.817L152.831 176.393Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M152.202 255.033L85.9927 228.456V150.391L152.202 255.033Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937"/>
|
||||
<path d="M220.927 148.946V228.74L153.25 255.905L85.5737 228.74V148.946L153.25 121.781L220.927 148.946Z" fill="black" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M220.927 148.946L153.25 176.111" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M153.25 255.906V176.111" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M153.25 176.111L85.5737 148.946" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<g style="mix-blend-mode:plus-lighter" opacity="0.8">
|
||||
<ellipse cx="190.193" cy="188.694" rx="69.2605" ry="106.046" transform="rotate(8.23956 190.193 188.694)" fill="#490631"/>
|
||||
</g>
|
||||
<g clip-path="url(#clip0_5423_1241)">
|
||||
<path d="M315.687 364.883L261.307 276.347C259.839 273.965 257.136 272.824 254.376 273.168L245.796 259.199L235.001 272.382L241.326 282.668L241.048 282.877C240.281 284.976 240.393 287.32 241.581 289.247L295.96 377.783C299.09 382.877 306.045 384.129 311.5 380.57C316.944 377.01 318.828 369.989 315.699 364.883H315.687Z" fill="black"/>
|
||||
<rect width="43.6018" height="4.6553" transform="matrix(0.955689 -0.387444 -0.0218247 0.993867 166.765 287.5)" fill="#F213A4"/>
|
||||
<path d="M206.796 82.6179C203.403 82.6179 200.067 82.9493 196.795 83.5262C193.701 82.9493 190.522 82.6179 187.241 82.6179C144.356 82.6179 110.562 130.096 105.356 188.67C100.149 247.244 120.629 294.722 168.387 294.722C171.57 294.722 174.693 294.427 177.773 293.924C181.021 294.439 184.401 294.722 187.954 294.722C230.765 294.722 264.632 247.244 269.839 188.67C275.046 130.096 248.225 82.6179 206.796 82.6179ZM192.082 289.64C190.853 289.726 189.603 289.775 188.357 289.775C143.88 289.775 124.907 244.507 129.869 188.682C134.832 132.857 166.394 87.5768 206.33 87.5768C207.391 87.5768 208.435 87.6259 209.479 87.6872C237.822 101.386 254.472 141.413 250.272 188.67C246.071 235.927 223.233 275.917 192.082 289.64Z" fill="black"/>
|
||||
<path d="M254.376 273.168C257.124 272.824 259.839 273.965 261.307 276.347L315.687 364.883C318.816 369.977 316.944 377.01 311.488 380.57C306.045 384.129 299.077 382.889 295.948 377.783L241.569 289.247C240.381 287.32 240.267 284.988 241.036 282.877" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M245.796 259.199L255.013 274.199C257.149 277.685 255.87 282.484 252.145 284.914C248.421 287.345 243.666 286.498 241.53 283.012L235.001 272.382" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M269.805 188.67C264.598 247.244 230.693 294.722 187.826 294.722C140.006 294.722 119.495 247.244 124.701 188.67C129.908 130.096 163.752 82.6179 206.68 82.6179C249.608 82.6179 275.012 130.096 269.805 188.67Z" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M168.246 294.722C120.426 294.722 99.9144 247.244 105.121 188.67C110.328 130.096 144.159 82.6179 187.1 82.6179" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M209.381 87.6755C237.761 101.386 254.426 141.413 250.225 188.67C246.024 235.927 223.028 276.163 191.713 289.775" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M264.789 188.67C259.826 244.507 228.156 289.763 188.242 289.763C143.708 289.763 124.705 244.494 129.667 188.67C134.629 132.845 166.226 87.5768 206.214 87.5768C246.202 87.5768 269.751 132.845 264.789 188.67Z" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M168.246 294.722H189.58" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M187.1 82.6179H208.434" stroke="#A3A3A3" stroke-width="0.837937" stroke-miterlimit="10"/>
|
||||
<path d="M183.882 227.367L160.043 237.032L160.145 232.404L183.983 222.74L183.882 227.367ZM208.333 206.919L160.043 226.496L160.145 221.869L208.435 202.292L208.333 206.919ZM208.334 196.137L160.042 215.715L160.144 211.088L208.435 191.511L208.334 196.137ZM192.454 191.882L160.043 205.021L160.145 200.394L192.556 187.254L192.454 191.882ZM208.333 174.907L160.043 194.485L160.145 189.858L208.435 170.281L208.333 174.907Z" fill="#F213A4"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5423_1241">
|
||||
<rect width="229.503" height="302.392" fill="white" transform="matrix(1 0 -0.0885398 0.996073 113.984 82.0041)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 380 KiB |
|
After Width: | Height: | Size: 77 KiB |
@@ -1,45 +1,38 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import thirdwebIconSrc from "@/../public/thirdweb.svg";
|
||||
import { Sidebar, type SidebarLink } from "@/components/ui/sidebar";
|
||||
import { ScrollShadow } from "../components/ui/ScrollShadow/ScrollShadow";
|
||||
import { otherLinks } from "./otherLinks";
|
||||
"use client";
|
||||
import { BookTextIcon, GithubIcon } from "lucide-react";
|
||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { FullWidthSidebarLayout } from "../components/blocks/full-width-sidebar-layout";
|
||||
import { DashboardIcon } from "../icons/DashboardIcon";
|
||||
import { sidebarLinks } from "./navLinks";
|
||||
|
||||
export function AppSidebar(props: { links: SidebarLink[] }) {
|
||||
export function AppSidebarLayout(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="z-10 hidden h-dvh w-[300px] flex-col border-border/50 border-r-2 xl:flex">
|
||||
<div className="border-b px-6 py-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<Image alt="thirdweb" className="size-6" src={thirdwebIconSrc} />
|
||||
<span className="font-bold text-lg leading-none tracking-tight">
|
||||
Playground
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative flex max-h-full flex-1 flex-col overflow-hidden">
|
||||
<ScrollShadow
|
||||
className="grow pr-4 pl-6"
|
||||
scrollableClassName="max-h-full pt-6"
|
||||
>
|
||||
<Sidebar links={props.links} />
|
||||
</ScrollShadow>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto flex flex-col gap-4 border-t px-6 py-6">
|
||||
{otherLinks.map((link) => {
|
||||
return (
|
||||
<Link
|
||||
className="flex items-center gap-2 text-muted-foreground hover:text-foreground "
|
||||
href={link.href}
|
||||
key={link.href}
|
||||
target="_blank"
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<SidebarProvider>
|
||||
<FullWidthSidebarLayout
|
||||
contentSidebarLinks={sidebarLinks}
|
||||
footerSidebarLinks={[
|
||||
{
|
||||
separator: true,
|
||||
},
|
||||
{
|
||||
href: "https://thirdweb.com/team?utm_source=playground",
|
||||
label: "Dashboard",
|
||||
icon: DashboardIcon,
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com?utm_source=playground",
|
||||
label: "Documentation",
|
||||
icon: BookTextIcon,
|
||||
},
|
||||
{
|
||||
href: "https://github.com/thirdweb-dev",
|
||||
label: "Github",
|
||||
icon: GithubIcon,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{props.children}
|
||||
</FullWidthSidebarLayout>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { MenuIcon, XIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import thirdwebIconSrc from "@/../public/thirdweb.svg";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { ScrollShadow } from "../components/ui/ScrollShadow/ScrollShadow";
|
||||
import { Sidebar, type SidebarLink } from "../components/ui/sidebar";
|
||||
import { otherLinks } from "./otherLinks";
|
||||
|
||||
export function MobileHeader(props: { links: SidebarLink[] }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "auto";
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = "auto";
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="sticky top-0 z-10 flex justify-between gap-4 border-b bg-background px-4 py-4 xl:hidden">
|
||||
<Link
|
||||
aria-label="thirdweb Docs"
|
||||
className="flex items-center gap-3"
|
||||
href="/"
|
||||
title="thirdweb Docs"
|
||||
>
|
||||
<Image alt="" className="size-7" src={thirdwebIconSrc} />
|
||||
<span className="font-bold text-xl leading-none tracking-tight">
|
||||
Playground
|
||||
</span>
|
||||
</Link>
|
||||
<Button
|
||||
className="!h-auto p-2"
|
||||
onClick={() => {
|
||||
setIsOpen((v) => !v);
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
{!isOpen ? (
|
||||
<MenuIcon className="size-6" />
|
||||
) : (
|
||||
<XIcon className="size-6" />
|
||||
)}
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
className="fade-in-0 slide-in-from-top-5 fixed top-[75px] right-0 bottom-0 left-0 z-50 flex animate-in flex-col bg-background duration-200"
|
||||
onClickCapture={(e) => {
|
||||
if (e.target instanceof HTMLElement && e.target.closest("a")) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="relative flex max-h-full flex-1 flex-col overflow-hidden">
|
||||
<ScrollShadow
|
||||
className="grow px-6"
|
||||
scrollableClassName="max-h-full pt-6"
|
||||
>
|
||||
<Sidebar links={props.links} />
|
||||
</ScrollShadow>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto flex flex-col gap-4 border-t px-6 py-6">
|
||||
{otherLinks.map((link) => {
|
||||
return (
|
||||
<Link
|
||||
className="flex items-center gap-2 text-muted-foreground hover:text-foreground "
|
||||
href={link.href}
|
||||
key={link.href}
|
||||
target="_blank"
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +1,33 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ShieldIcon } from "lucide-react";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import { ConnectSmartAccountCustomPreview } from "../../../../components/account-abstraction/connect-smart-account";
|
||||
import { SponsoredTxPreview } from "../../../../components/account-abstraction/sponsored-tx";
|
||||
import { PageLayout } from "../../../../components/blocks/APIHeader";
|
||||
import { CodeExample } from "../../../../components/code/code-example";
|
||||
import { SponsoredInAppTxPreview } from "../../../../components/in-app-wallet/sponsored-tx";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
import { ConnectSmartAccountCustomPreview } from "../../../components/account-abstraction/connect-smart-account";
|
||||
import { SponsoredTxPreview } from "../../../components/account-abstraction/sponsored-tx";
|
||||
import { PageLayout } from "../../../components/blocks/APIHeader";
|
||||
import { CodeExample } from "../../../components/code/code-example";
|
||||
import { SponsoredInAppTxPreview } from "../../../components/in-app-wallet/sponsored-tx";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description: "Turn any EOA into a smart contract wallet with EIP-4337.",
|
||||
metadataBase,
|
||||
title: "EIP-4337 Smart Contract Wallets | thirdweb Connect",
|
||||
};
|
||||
const title = "Account Abstraction EIP-4337";
|
||||
const description =
|
||||
"Enable account abstraction with EIP-4337. Unlock gasless transactions, session keys, paymaster support, and smart wallet programmability";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description,
|
||||
title,
|
||||
image: {
|
||||
icon: "wallets",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>Turn any EOA into a smart contract wallet with EIP-4337.</>
|
||||
}
|
||||
icon={ShieldIcon}
|
||||
title={title}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/wallets/sponsor-gas?utm_source=playground"
|
||||
title="EIP-4337 Smart Contract Wallets"
|
||||
>
|
||||
<div className="flex flex-col gap-14">
|
||||
<SponsoredTx />
|
||||
@@ -1,30 +1,32 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ShieldIcon } from "lucide-react";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import { Eip5792GetCapabilitiesPreview } from "../../../../components/account-abstraction/5792-get-capabilities";
|
||||
import { Eip5792SendCallsPreview } from "../../../../components/account-abstraction/5792-send-calls";
|
||||
import { PageLayout } from "../../../../components/blocks/APIHeader";
|
||||
import { CodeExample } from "../../../../components/code/code-example";
|
||||
import { Eip5792GetCapabilitiesPreview } from "../../../components/account-abstraction/5792-get-capabilities";
|
||||
import { Eip5792SendCallsPreview } from "../../../components/account-abstraction/5792-send-calls";
|
||||
import { PageLayout } from "../../../components/blocks/APIHeader";
|
||||
import { CodeExample } from "../../../components/code/code-example";
|
||||
import { createMetadata } from "../../../lib/metadata";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description:
|
||||
"EIP-5792 capabilities allow you to view the capabilities of the connected wallet",
|
||||
metadataBase,
|
||||
title: "EIP-5792 Wallet Capabilities | thirdweb Connect",
|
||||
};
|
||||
const title = "Account Abstraction EIP-5792";
|
||||
const description =
|
||||
"EIP-5792 brings a standard for abstracted actions—combine transaction batching, bundling, and paymasters with a unified smart account interface";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description,
|
||||
title,
|
||||
image: {
|
||||
icon: "wallets",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>
|
||||
EIP-5792 capabilities allow you to view the capabilities of the
|
||||
connected wallet.
|
||||
</>
|
||||
}
|
||||
icon={ShieldIcon}
|
||||
title={title}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/wallets/sponsor-gas?utm_source=playground"
|
||||
title="EIP-5792 Wallet Capabilities"
|
||||
>
|
||||
<div className="flex flex-col gap-14">
|
||||
<Eip5792GetCapabilities />
|
||||
@@ -1,29 +1,33 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ShieldIcon } from "lucide-react";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import { Eip7702SmartAccountPreview } from "../../../../components/account-abstraction/7702-smart-account";
|
||||
import { PageLayout } from "../../../../components/blocks/APIHeader";
|
||||
import { CodeExample } from "../../../../components/code/code-example";
|
||||
import { Eip7702SmartAccountPreview } from "../../../components/account-abstraction/7702-smart-account";
|
||||
import { PageLayout } from "../../../components/blocks/APIHeader";
|
||||
import { CodeExample } from "../../../components/code/code-example";
|
||||
import { createMetadata } from "../../../lib/metadata";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description:
|
||||
"EIP-7702 smart accounts allow you to turn your EOA into a smart account with no code changes",
|
||||
metadataBase,
|
||||
title: "EIP-7702 Smart Accounts | thirdweb Connect",
|
||||
};
|
||||
const title = "Account Abstraction EIP-7702";
|
||||
const description =
|
||||
"Enable account abstraction with EIP-7702. Unlock gasless transactions, session keys, paymaster support, and smart wallet programmability";
|
||||
const ogDescription =
|
||||
"Use EIP-7702 to upgrade EOAs into temporary smart accounts. Bring native account abstraction to any wallet, enabling gasless transactions and advanced logic.";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description: ogDescription,
|
||||
title,
|
||||
image: {
|
||||
icon: "wallets",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>
|
||||
EIP-7702 smart accounts allow you to turn your EOA into a smart
|
||||
account with no code changes.
|
||||
</>
|
||||
}
|
||||
icon={ShieldIcon}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/wallets/sponsor-gas?utm_source=playground"
|
||||
title="EIP-7702 Smart Accounts"
|
||||
title={title}
|
||||
>
|
||||
<Eip7702SmartAccount />
|
||||
</PageLayout>
|
||||
@@ -1,29 +1,31 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ShieldIcon } from "lucide-react";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import { SponsoredTxZksyncPreview } from "../../../../components/account-abstraction/sponsored-tx-zksync";
|
||||
import { PageLayout } from "../../../../components/blocks/APIHeader";
|
||||
import { CodeExample } from "../../../../components/code/code-example";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
import { SponsoredTxZksyncPreview } from "../../../components/account-abstraction/sponsored-tx-zksync";
|
||||
import { PageLayout } from "../../../components/blocks/APIHeader";
|
||||
import { CodeExample } from "../../../components/code/code-example";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description:
|
||||
"On zkSync chains, you can take advantage of native account abstraction with no code changes",
|
||||
metadataBase,
|
||||
title: "Native Account Abstraction",
|
||||
};
|
||||
const title = "Native Account Abstraction Through zkSync";
|
||||
const description =
|
||||
"Leverage native account abstraction on zkSync. Use smart accounts out of the box for gasless transactions, batching, and flexible signing logic";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description,
|
||||
title,
|
||||
image: {
|
||||
icon: "wallets",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>
|
||||
On zkSync chains, you can take advantage of native account
|
||||
abstraction with no code changes.
|
||||
</>
|
||||
}
|
||||
icon={ShieldIcon}
|
||||
title={title}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/wallets/sponsor-gas?utm_source=playground"
|
||||
title="Native Account Abstraction"
|
||||
>
|
||||
<SponsoredZksyncTx />
|
||||
</PageLayout>
|
||||
@@ -0,0 +1,108 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
import { ImageResponse } from "next/og";
|
||||
import { getBaseUrl } from "@/lib/env";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
const BASE_URL = getBaseUrl();
|
||||
|
||||
const width = 1200;
|
||||
const height = 630;
|
||||
|
||||
const iconSize = 400;
|
||||
|
||||
const inter600 = fetch(new URL("./inter/700.ttf", import.meta.url)).then(
|
||||
(res) => res.arrayBuffer(),
|
||||
);
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
const icon = searchParams.get("icon");
|
||||
const title = searchParams.get("title");
|
||||
|
||||
if (!icon || !title) {
|
||||
return new Response("Failed to generate the image", {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
|
||||
const iconUrl = `${BASE_URL}/og/icons/${icon}.svg`;
|
||||
|
||||
return new ImageResponse(
|
||||
// ImageResponse JSX element
|
||||
<div
|
||||
style={{
|
||||
alignItems: "center",
|
||||
background: "black",
|
||||
display: "flex",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
height={height}
|
||||
src={`${BASE_URL}/og/background-1.jpg`}
|
||||
style={{
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 0,
|
||||
}}
|
||||
width={width}
|
||||
/>
|
||||
|
||||
{/* Left */}
|
||||
<div
|
||||
style={{
|
||||
color: "white",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
padding: "80px 60px",
|
||||
width: "50%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
fontFamily: "Inter",
|
||||
fontSize: "60px",
|
||||
fontWeight: 600,
|
||||
marginBottom: "40px",
|
||||
wordBreak: title.includes(" ") ? "break-word" : "break-all",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img
|
||||
alt=""
|
||||
height={iconSize}
|
||||
src={iconUrl}
|
||||
style={{
|
||||
display: "flex",
|
||||
position: "absolute",
|
||||
right: width / 20,
|
||||
top: height / 2 - iconSize / 1.5,
|
||||
}}
|
||||
width={iconSize}
|
||||
/>
|
||||
</div>,
|
||||
// ImageResponse options
|
||||
{
|
||||
fonts: [
|
||||
{
|
||||
data: await inter600,
|
||||
name: "Inter",
|
||||
style: "normal",
|
||||
weight: 600,
|
||||
},
|
||||
],
|
||||
height: height,
|
||||
width: width,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { RssIcon } from "lucide-react";
|
||||
import { WatchEventPreview } from "@/components/blockchain-api/watch-event-preview";
|
||||
import { CodeExample } from "@/components/code/code-example";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { PageLayout } from "../../../components/blocks/APIHeader";
|
||||
import { createMetadata } from "../../../lib/metadata";
|
||||
|
||||
const title = "Listen Contract Events";
|
||||
const description =
|
||||
"Subscribe to any contract event with auto-polling hooks and type-safe event handlers. Supports all common standards out of the box";
|
||||
|
||||
const ogDescription =
|
||||
"Listen to blockchain contract events using auto-polling hooks and type-safe functions. Supports ERC20, ERC721, and other common Web3 standards with minimal setup.";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title,
|
||||
description: ogDescription,
|
||||
image: {
|
||||
icon: "contract",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
icon={RssIcon}
|
||||
title={title}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/typescript/v5?utm_source=playground"
|
||||
>
|
||||
<WatchEvent />
|
||||
</PageLayout>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function WatchEvent() {
|
||||
return (
|
||||
<CodeExample
|
||||
code={`import { useContractEvents } from "thirdweb/react";
|
||||
import { getContract } from "thirdweb";
|
||||
import { base } from "thirdweb/chains";
|
||||
import { transferEvent } from "thirdweb/extensions/erc20";
|
||||
|
||||
const usdcContractOnBase = getContract({
|
||||
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
||||
chain: base,
|
||||
client,
|
||||
});
|
||||
|
||||
function App() {
|
||||
// Listen to USDC transfers on Base
|
||||
const contractEvents = useContractEvents({
|
||||
contract: usdcContractOnBase,
|
||||
events: [transferEvent()],
|
||||
blockRange: 100,
|
||||
});
|
||||
|
||||
(contractEvents.data || []).forEach((item) => {
|
||||
const { from, to, value } = item.args;
|
||||
console.log("{from}...{value} USDC...{to}");
|
||||
});
|
||||
}
|
||||
`}
|
||||
lang="tsx"
|
||||
preview={<WatchEventPreview />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { BlocksIcon } from "lucide-react";
|
||||
import { ReadContractExtensionPreview } from "@/components/blockchain-api/read-contract-extension";
|
||||
import { WriteContractExtensionPreview } from "@/components/blockchain-api/write-contract-extension";
|
||||
import { CodeExample } from "@/components/code/code-example";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { PageLayout } from "../../../components/blocks/APIHeader";
|
||||
import { createMetadata } from "../../../lib/metadata";
|
||||
|
||||
const title = "Pre-Built Extensions";
|
||||
const description =
|
||||
"High-level ready and write functions with built-in pre/post-processing for common standards.";
|
||||
|
||||
const ogDescription =
|
||||
"Simplify smart contract reads and writes with prebuilt extensions. Handle approvals, formatting, and standards with built-in pre/post-processing.";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title,
|
||||
description: ogDescription,
|
||||
image: {
|
||||
icon: "contract",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
icon={BlocksIcon}
|
||||
title={title}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/typescript/v5?utm_source=playground"
|
||||
>
|
||||
<div className="flex flex-col gap-14">
|
||||
<ReadContractExtension />
|
||||
<WriteContractExtension />
|
||||
</div>
|
||||
</PageLayout>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ReadContractExtension() {
|
||||
return (
|
||||
<CodeExample
|
||||
code={`import { getContract } from "thirdweb";
|
||||
import { ethereum } from "thirdweb/chains";
|
||||
import { MediaRenderer, useReadContract } from "thirdweb/react";
|
||||
|
||||
const nftContract = getContract({
|
||||
address: "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d",
|
||||
chain: ethereum,
|
||||
client: THIRDWEB_CLIENT,
|
||||
});
|
||||
|
||||
function App() {
|
||||
const { data } = useReadContract(getNFT, {
|
||||
contract: nftContract,
|
||||
tokenId: 458n,
|
||||
});
|
||||
|
||||
return (
|
||||
<MediaRenderer
|
||||
client={THIRDWEB_CLIENT}
|
||||
src={data?.metadata.image}
|
||||
/>
|
||||
);
|
||||
}
|
||||
`}
|
||||
header={{
|
||||
description:
|
||||
"Extensions let you do more with less code. High level functions with simple API that do pre and post processing for all common standards.",
|
||||
title: "Prebuilt read extensions",
|
||||
}}
|
||||
lang="tsx"
|
||||
preview={<ReadContractExtensionPreview />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function WriteContractExtension() {
|
||||
return (
|
||||
<CodeExample
|
||||
code={`import { getContract } from "thirdweb";
|
||||
import { sepolia } from "thirdweb/chains";
|
||||
import { claimTo } from "thirdweb/extensions/erc20";
|
||||
|
||||
const tw_coin = getContract({
|
||||
address: "0xACf072b740a23D48ECd302C9052fbeb3813b60a6",
|
||||
chain: sepolia,
|
||||
client: THIRDWEB_CLIENT,
|
||||
});
|
||||
|
||||
function App() {
|
||||
return <TransactionButton
|
||||
transaction={() =>
|
||||
claimTo({
|
||||
contract: twCoinContract,
|
||||
to: account.address,
|
||||
quantity: "10",
|
||||
})
|
||||
}
|
||||
>
|
||||
Claim
|
||||
</TransactionButton>
|
||||
}
|
||||
`}
|
||||
header={{
|
||||
description:
|
||||
"Extensions let you do more with less code. High level functions with simple API that do pre and post processing for all common standards.",
|
||||
title: "Prebuilt write extensions",
|
||||
}}
|
||||
lang="tsx"
|
||||
preview={<WriteContractExtensionPreview />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { ScanTextIcon } from "lucide-react";
|
||||
import { ReadContractRawPreview } from "@/components/blockchain-api/read-contract-raw";
|
||||
import { PageLayout } from "@/components/blocks/APIHeader";
|
||||
import { CodeExample } from "@/components/code/code-example";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
|
||||
const title = "Read Contract Data";
|
||||
const description =
|
||||
"Read data from any contract on EVM with Type safe functions and hooks without needing contract ABIs";
|
||||
const ogDescription =
|
||||
"Query smart contracts and wallet data using type-safe functions and React hooks—no ABI required. Read blockchain data with full type safety";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title,
|
||||
description: ogDescription,
|
||||
image: {
|
||||
icon: "contract",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
icon={ScanTextIcon}
|
||||
title={title}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/typescript/v5?utm_source=playground"
|
||||
>
|
||||
<div className="flex flex-col gap-14">
|
||||
<ReadContractRaw />
|
||||
</div>
|
||||
</PageLayout>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ReadContractRaw() {
|
||||
return (
|
||||
<CodeExample
|
||||
code={`import { getContract } from "thirdweb";
|
||||
import { ethereum } from "thirdweb/chains";
|
||||
import { MediaRenderer, useReadContract } from "thirdweb/react";
|
||||
|
||||
const onChainCryptoPunks = getContract({
|
||||
address: "0x16F5A35647D6F03D5D3da7b35409D65ba03aF3B2",
|
||||
chain: ethereum,
|
||||
client: THIRDWEB_CLIENT,
|
||||
});
|
||||
|
||||
function App() {
|
||||
// Read the image of the tokenId #1
|
||||
const { data } = useReadContract({
|
||||
contract: onChainCryptoPunks,
|
||||
method: "function punkImageSvg(uint16 index) view returns (string svg)",
|
||||
params: [1],
|
||||
});
|
||||
|
||||
return (
|
||||
<MediaRenderer
|
||||
client={THIRDWEB_CLIENT}
|
||||
src={data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
`}
|
||||
lang="tsx"
|
||||
preview={<ReadContractRawPreview />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { PencilIcon } from "lucide-react";
|
||||
import { WriteContractRawPreview } from "@/components/blockchain-api/write-contract-raw";
|
||||
import { CodeExample } from "@/components/code/code-example";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { PageLayout } from "../../../components/blocks/APIHeader";
|
||||
import { createMetadata } from "../../../lib/metadata";
|
||||
|
||||
const title = "Write Contract";
|
||||
const description =
|
||||
"Send transactions from the connected wallet using type-safe functions and hooks for contract calls or raw transactions";
|
||||
|
||||
const ogDescription =
|
||||
"Send blockchain transactions with the connected wallet using type-safe functions and React hooks for contract calls or raw transactions";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title,
|
||||
description: ogDescription,
|
||||
image: {
|
||||
icon: "contract",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
icon={PencilIcon}
|
||||
title={title}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/typescript/v5?utm_source=playground"
|
||||
>
|
||||
<div className="flex flex-col gap-14">
|
||||
<WriteContractRaw />
|
||||
</div>
|
||||
</PageLayout>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function WriteContractRaw() {
|
||||
return (
|
||||
<CodeExample
|
||||
code={`import { getContract, prepareContractCall, toUnits } from "thirdweb";
|
||||
import { sepolia } from "thirdweb/chains";
|
||||
|
||||
const tw_coin = getContract({
|
||||
address: "0xACf072b740a23D48ECd302C9052fbeb3813b60a6",
|
||||
chain: sepolia,
|
||||
client: THIRDWEB_CLIENT,
|
||||
});
|
||||
|
||||
|
||||
function App() {
|
||||
return <TransactionButton
|
||||
transaction={() =>
|
||||
prepareContractCall({
|
||||
contract: tw_coin,
|
||||
method:
|
||||
"function transfer(address to, uint256 value) returns (bool)",
|
||||
params: [
|
||||
"0x...",
|
||||
toUnits("5", 18),
|
||||
],
|
||||
})
|
||||
}
|
||||
>
|
||||
Send
|
||||
</TransactionButton>
|
||||
}
|
||||
`}
|
||||
lang="tsx"
|
||||
preview={<WriteContractRawPreview />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -108,6 +108,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.shiki,
|
||||
.shiki span {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.dark .shiki,
|
||||
.dark .shiki span {
|
||||
color: var(--shiki-dark) !important;
|
||||
@@ -117,11 +122,6 @@
|
||||
text-decoration: var(--shiki-dark-text-decoration) !important;
|
||||
}
|
||||
|
||||
.shiki,
|
||||
.shiki span {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* Fix colors on auto-filled inputs */
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
@@ -149,3 +149,8 @@ input:-webkit-autofill:active {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: hsl(var(--muted)) transparent;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export default async function Page(props: {
|
||||
return (
|
||||
<div>
|
||||
<Breadcrumbs />
|
||||
<h1 className="mt-3 mb-6 font-semibold text-2xl tracking-tight lg:text-3xl">
|
||||
<h1 className="mt-3 mb-6 font-semibold text-2xl tracking-tight">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
import type React from "react";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
import { PageLayout } from "../../components/blocks/APIHeader";
|
||||
import { InsightIcon } from "../../icons/InsightIcon";
|
||||
|
||||
const title = "Insight";
|
||||
const description =
|
||||
"Simple & customizable endpoints for querying rich blockchain data";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title,
|
||||
description,
|
||||
image: {
|
||||
icon: "insight",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Layout(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<PageLayout
|
||||
icon={InsightIcon}
|
||||
containerClassName="pb-16"
|
||||
description={
|
||||
<>Simple & customizable endpoints for querying rich blockchain data</>
|
||||
}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/insight?utm_source=playground"
|
||||
title="Insight"
|
||||
title={title}
|
||||
>
|
||||
{props.children}
|
||||
</PageLayout>
|
||||
|
||||
@@ -2,12 +2,11 @@ import type { Metadata } from "next";
|
||||
import { Fira_Code, Inter } from "next/font/google";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AppSidebar } from "./AppSidebar";
|
||||
import { AppSidebarLayout } from "./AppSidebar";
|
||||
import { Providers } from "./providers";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import NextTopLoader from "nextjs-toploader";
|
||||
import { MobileHeader } from "./MobileHeader";
|
||||
import { getSidebarLinks } from "./navLinks";
|
||||
|
||||
const sansFont = Inter({
|
||||
subsets: ["latin"],
|
||||
@@ -32,7 +31,6 @@ export default async function RootLayout({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const sidebarLinks = getSidebarLinks();
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
@@ -42,21 +40,24 @@ export default async function RootLayout({
|
||||
monoFont.variable,
|
||||
)}
|
||||
>
|
||||
<NextTopLoader
|
||||
color="hsl(var(--foreground))"
|
||||
height={2}
|
||||
shadow={false}
|
||||
showSpinner={false}
|
||||
/>
|
||||
<MobileHeader links={sidebarLinks} />
|
||||
<div className="flex flex-col lg:h-dvh lg:flex-row">
|
||||
<AppSidebar links={sidebarLinks} />
|
||||
<div className="flex grow flex-col lg:overflow-auto">
|
||||
<div className="relative grow">
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
disableTransitionOnChange
|
||||
enableSystem={false}
|
||||
>
|
||||
<NextTopLoader
|
||||
color="hsl(var(--foreground))"
|
||||
height={2}
|
||||
shadow={false}
|
||||
showSpinner={false}
|
||||
/>
|
||||
<div className="flex flex-col lg:h-dvh lg:flex-row">
|
||||
<AppSidebarLayout>
|
||||
<Providers>{children}</Providers>
|
||||
</div>
|
||||
</AppSidebarLayout>
|
||||
</div>
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
// TODO: clean all of this up a heck of a lot!
|
||||
|
||||
const LOGIN_URL = "https://login.thirdweb.com";
|
||||
const CLIENT_ID = "demo";
|
||||
|
||||
export async function triggerLogin() {
|
||||
const { codeChallenge, codeVerifier } = await generateCodeChallenge();
|
||||
const state = generateState();
|
||||
storeCodeVerifier(codeVerifier);
|
||||
storeState(state);
|
||||
const redirectUri = window.location.href;
|
||||
window.location.href = `${LOGIN_URL}/authorize?client_id=${CLIENT_ID}&redirect_uri=${redirectUri}&response_type=code&state=${state}&code_challenge=${codeChallenge}&code_challenge_method=S256`;
|
||||
}
|
||||
|
||||
// do code exchange
|
||||
export async function handleLogin(code: string) {
|
||||
const codeVerifier = getCodeVerifier();
|
||||
const state = getState();
|
||||
if (!codeVerifier || !state) {
|
||||
console.error("No code verifier or state found");
|
||||
return;
|
||||
}
|
||||
// clear the code verifier and state -> we don't need them anymore
|
||||
clearCodeVerifier();
|
||||
clearState();
|
||||
const redirectUri = window.location.href;
|
||||
|
||||
fetch(`${LOGIN_URL}/api/token`, {
|
||||
body: JSON.stringify({
|
||||
client_id: CLIENT_ID,
|
||||
code,
|
||||
code_verifier: codeVerifier,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: redirectUri,
|
||||
state,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
console.log("data", data);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("error", err);
|
||||
});
|
||||
}
|
||||
|
||||
// oauth2 PKCE code challenge generator (code challenge has to be url safe)
|
||||
async function generateCodeChallenge() {
|
||||
const codeVerifier = base64UrlEncode(
|
||||
window.crypto.getRandomValues(new Uint8Array(64)),
|
||||
);
|
||||
const codeChallenge = await window.crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(codeVerifier),
|
||||
);
|
||||
return {
|
||||
codeChallenge: base64UrlEncode(codeChallenge),
|
||||
codeVerifier: codeVerifier,
|
||||
};
|
||||
}
|
||||
|
||||
function generateState() {
|
||||
const random = window.crypto.getRandomValues(new Uint8Array(12));
|
||||
return base64UrlEncode(random);
|
||||
}
|
||||
|
||||
function base64UrlEncode(array: Uint8Array | ArrayBuffer) {
|
||||
return btoa(String.fromCharCode(...new Uint8Array(array)))
|
||||
.replace(/=/g, "")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_");
|
||||
}
|
||||
|
||||
function storeCodeVerifier(codeVerifier: string) {
|
||||
localStorage.setItem("codeVerifier", codeVerifier);
|
||||
}
|
||||
function getCodeVerifier() {
|
||||
return localStorage.getItem("codeVerifier");
|
||||
}
|
||||
function clearCodeVerifier() {
|
||||
localStorage.removeItem("codeVerifier");
|
||||
}
|
||||
function storeState(state: string) {
|
||||
localStorage.setItem("state", state);
|
||||
}
|
||||
function getState() {
|
||||
return localStorage.getItem("state");
|
||||
}
|
||||
function clearState() {
|
||||
localStorage.removeItem("state");
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Suspense } from "react";
|
||||
|
||||
export default function LoginLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return <Suspense fallback={<div>Loading...</div>}>{children}</Suspense>;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { handleLogin, triggerLogin } from "./_sdk_";
|
||||
|
||||
export default function LoginPage() {
|
||||
const searchParams = useSearchParams();
|
||||
const code = searchParams.get("code");
|
||||
if (code) {
|
||||
handleLogin(code);
|
||||
}
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Login Test</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button
|
||||
onClick={() => {
|
||||
triggerLogin();
|
||||
}}
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,163 +1,225 @@
|
||||
import type { SidebarLink } from "../components/ui/sidebar";
|
||||
"use client";
|
||||
|
||||
import { ArrowLeftRightIcon } from "lucide-react";
|
||||
import type { ShadcnSidebarLink } from "@/components/blocks/full-width-sidebar-layout";
|
||||
import { ContractIcon } from "../icons/ContractIcon";
|
||||
import { InsightIcon } from "../icons/InsightIcon";
|
||||
import { PayIcon } from "../icons/PayIcon";
|
||||
import { SmartAccountIcon } from "../icons/SmartAccountIcon";
|
||||
import { TokenIcon } from "../icons/TokenIcon";
|
||||
import { WalletProductIcon } from "../icons/WalletProductIcon";
|
||||
import { insightBlueprints } from "./insight/insightBlueprints";
|
||||
|
||||
const staticSidebarLinks: SidebarLink[] = [
|
||||
{
|
||||
isCollapsible: false,
|
||||
links: [
|
||||
{
|
||||
href: "/wallets/sign-in/button",
|
||||
name: "ConnectButton",
|
||||
},
|
||||
{
|
||||
href: "/wallets/sign-in/embed",
|
||||
name: "ConnectEmbed",
|
||||
},
|
||||
{
|
||||
href: "/wallets/sign-in/headless",
|
||||
name: "Headless Connect",
|
||||
},
|
||||
{
|
||||
href: "/wallets/in-app-wallet",
|
||||
name: "In-App Wallets",
|
||||
},
|
||||
{
|
||||
href: "/wallets/in-app-wallet/ecosystem",
|
||||
name: "Ecosystem Wallets",
|
||||
},
|
||||
{
|
||||
href: "/wallets/account-abstraction/eip-4337",
|
||||
name: "EIP-4337",
|
||||
},
|
||||
{
|
||||
href: "/wallets/account-abstraction/eip-7702",
|
||||
name: "EIP-7702",
|
||||
},
|
||||
{
|
||||
href: "/wallets/account-abstraction/eip-5792",
|
||||
name: "EIP-5792",
|
||||
},
|
||||
{
|
||||
href: "/wallets/account-abstraction/native-aa",
|
||||
name: "Native AA (zkSync)",
|
||||
},
|
||||
{
|
||||
href: "/wallets/auth",
|
||||
name: "Auth",
|
||||
},
|
||||
{
|
||||
href: "/wallets/social",
|
||||
name: "Social",
|
||||
},
|
||||
{
|
||||
href: "/wallets/blockchain-api",
|
||||
name: "Blockchain API",
|
||||
},
|
||||
{
|
||||
expanded: false,
|
||||
links: [
|
||||
{
|
||||
href: "/wallets/headless/account-components",
|
||||
name: "Account",
|
||||
},
|
||||
{
|
||||
href: "/wallets/headless/nft-components",
|
||||
name: "NFT",
|
||||
},
|
||||
{
|
||||
href: "/wallets/headless/token-components",
|
||||
name: "Token",
|
||||
},
|
||||
{
|
||||
href: "/wallets/headless/chain-components",
|
||||
name: "Chain",
|
||||
},
|
||||
{
|
||||
href: "/wallets/headless/wallet-components",
|
||||
name: "Wallet",
|
||||
},
|
||||
],
|
||||
name: "Headless Components",
|
||||
},
|
||||
],
|
||||
name: "Wallets",
|
||||
const wallets: ShadcnSidebarLink = {
|
||||
subMenu: {
|
||||
label: "Wallets",
|
||||
icon: WalletProductIcon,
|
||||
},
|
||||
];
|
||||
links: [
|
||||
{
|
||||
href: "/wallets/sign-in/button",
|
||||
label: "Connect Button",
|
||||
},
|
||||
{
|
||||
href: "/wallets/sign-in/embed",
|
||||
label: "Connect Embed",
|
||||
},
|
||||
{
|
||||
href: "/wallets/sign-in/headless",
|
||||
label: "Headless Connect",
|
||||
},
|
||||
{
|
||||
href: "/wallets/in-app-wallet",
|
||||
label: "In-App Wallets",
|
||||
},
|
||||
{
|
||||
href: "/wallets/ecosystem-wallet",
|
||||
label: "Ecosystem Wallets",
|
||||
},
|
||||
{
|
||||
href: "/wallets/auth",
|
||||
label: "Authentication (SIWE)",
|
||||
},
|
||||
{
|
||||
href: "/wallets/social",
|
||||
label: "Social Profiles",
|
||||
},
|
||||
|
||||
const universalBridgeSidebarLinks: SidebarLink = {
|
||||
expanded: false,
|
||||
isCollapsible: false,
|
||||
{
|
||||
subMenu: {
|
||||
label: "Headless Components",
|
||||
},
|
||||
links: [
|
||||
{
|
||||
href: "/wallets/headless/account-components",
|
||||
label: "Account Components",
|
||||
},
|
||||
{
|
||||
href: "/wallets/headless/chain-components",
|
||||
label: "Chain Components",
|
||||
},
|
||||
{
|
||||
href: "/wallets/headless/wallet-components",
|
||||
label: "Wallet Components",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const contracts: ShadcnSidebarLink = {
|
||||
subMenu: {
|
||||
label: "Contracts",
|
||||
icon: ContractIcon,
|
||||
},
|
||||
links: [
|
||||
{
|
||||
href: "/contracts/read",
|
||||
label: "Read Contract",
|
||||
},
|
||||
{
|
||||
href: "/contracts/write",
|
||||
label: "Write Contract",
|
||||
},
|
||||
{
|
||||
href: "/contracts/extensions",
|
||||
label: "Pre-built Extensions",
|
||||
},
|
||||
{
|
||||
href: "/contracts/events",
|
||||
label: "Listen Contract Events",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const tokens: ShadcnSidebarLink = {
|
||||
subMenu: {
|
||||
label: "Tokens",
|
||||
icon: TokenIcon,
|
||||
},
|
||||
links: [
|
||||
{
|
||||
href: "/tokens/token-components",
|
||||
label: "Token Components",
|
||||
},
|
||||
{
|
||||
href: "/tokens/nft-components",
|
||||
label: "NFT Components",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const accountAbstractions: ShadcnSidebarLink = {
|
||||
subMenu: {
|
||||
label: "Account Abstraction",
|
||||
icon: SmartAccountIcon,
|
||||
},
|
||||
links: [
|
||||
{
|
||||
href: "/account-abstraction/eip-4337",
|
||||
label: "EIP-4337",
|
||||
},
|
||||
{
|
||||
href: "/account-abstraction/eip-7702",
|
||||
label: "EIP-7702",
|
||||
},
|
||||
{
|
||||
href: "/account-abstraction/eip-5792",
|
||||
label: "EIP-5792",
|
||||
},
|
||||
{
|
||||
href: "/account-abstraction/native-aa",
|
||||
label: "Native AA (zkSync)",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const payments: ShadcnSidebarLink = {
|
||||
subMenu: {
|
||||
label: "Payments",
|
||||
icon: PayIcon,
|
||||
},
|
||||
links: [
|
||||
{
|
||||
href: "/payments/ui-components",
|
||||
name: "UI Component",
|
||||
label: "UI Components",
|
||||
},
|
||||
{
|
||||
href: "/payments/fund-wallet",
|
||||
name: "Buy Crypto",
|
||||
label: "Buy Crypto",
|
||||
},
|
||||
{
|
||||
href: "/payments/commerce",
|
||||
name: "Checkout",
|
||||
label: "Checkout",
|
||||
},
|
||||
{
|
||||
href: "/payments/transactions",
|
||||
name: "Transactions",
|
||||
label: "Onchain Transaction",
|
||||
},
|
||||
{
|
||||
href: "/payments/backend",
|
||||
name: "Backend API",
|
||||
label: "Payments API",
|
||||
},
|
||||
],
|
||||
name: "Payments",
|
||||
};
|
||||
|
||||
const engineSidebarLinks: SidebarLink = {
|
||||
expanded: false,
|
||||
isCollapsible: false,
|
||||
const transactions: ShadcnSidebarLink = {
|
||||
subMenu: {
|
||||
label: "Transactions",
|
||||
icon: ArrowLeftRightIcon,
|
||||
},
|
||||
links: [
|
||||
{
|
||||
href: "/transactions/airdrop-tokens",
|
||||
name: "Airdrop",
|
||||
label: "Airdrop Tokens",
|
||||
},
|
||||
{
|
||||
href: "/transactions/mint-tokens",
|
||||
name: "Mint NFTs",
|
||||
label: "Mint NFTs",
|
||||
},
|
||||
{
|
||||
href: "/transactions/webhooks",
|
||||
name: "Webhooks",
|
||||
label: "Webhooks",
|
||||
},
|
||||
],
|
||||
name: "Transactions",
|
||||
};
|
||||
|
||||
export function getSidebarLinks() {
|
||||
const insightLinks: SidebarLink[] = insightBlueprints.map((blueprint) => {
|
||||
return {
|
||||
expanded: false,
|
||||
links: blueprint.paths.map((pathInfo) => {
|
||||
return {
|
||||
crossedOut: pathInfo.deprecated,
|
||||
href: `/insight/${blueprint.id}?path=${pathInfo.path}`,
|
||||
name: pathInfo.name,
|
||||
};
|
||||
}),
|
||||
name: blueprint.name,
|
||||
};
|
||||
});
|
||||
|
||||
const sidebarLinks: SidebarLink[] = [
|
||||
...staticSidebarLinks,
|
||||
universalBridgeSidebarLinks,
|
||||
engineSidebarLinks,
|
||||
{
|
||||
expanded: false,
|
||||
isCollapsible: false,
|
||||
links: insightLinks,
|
||||
name: "Insight",
|
||||
const insightLinks: ShadcnSidebarLink[] = insightBlueprints.map((blueprint) => {
|
||||
return {
|
||||
links: blueprint.paths.map((pathInfo) => {
|
||||
return {
|
||||
href: `/insight/${blueprint.id}?path=${pathInfo.path}`,
|
||||
label: pathInfo.name,
|
||||
exactMatch: true,
|
||||
};
|
||||
}),
|
||||
subMenu: {
|
||||
label: blueprint.name,
|
||||
},
|
||||
];
|
||||
};
|
||||
});
|
||||
|
||||
return sidebarLinks;
|
||||
}
|
||||
const insight: ShadcnSidebarLink = {
|
||||
links: [
|
||||
{
|
||||
href: "/insight",
|
||||
label: "Overview",
|
||||
exactMatch: true,
|
||||
},
|
||||
...insightLinks,
|
||||
],
|
||||
subMenu: {
|
||||
label: "Insight",
|
||||
icon: InsightIcon,
|
||||
},
|
||||
};
|
||||
|
||||
export const sidebarLinks: ShadcnSidebarLink[] = [
|
||||
wallets,
|
||||
transactions,
|
||||
contracts,
|
||||
payments,
|
||||
tokens,
|
||||
insight,
|
||||
accountAbstractions,
|
||||
];
|
||||
|
||||
|
After Width: | Height: | Size: 61 KiB |
@@ -1,14 +0,0 @@
|
||||
export const otherLinks: { name: string; href: string }[] = [
|
||||
{
|
||||
href: "https://thirdweb.com/team?utm_source=playground",
|
||||
name: "Dashboard",
|
||||
},
|
||||
{
|
||||
href: "https://portal.thirdweb.com?utm_source=playground",
|
||||
name: "Docs",
|
||||
},
|
||||
{
|
||||
href: "https://github.com/thirdweb-dev",
|
||||
name: "Github",
|
||||
},
|
||||
];
|
||||
@@ -1,5 +1,294 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import {
|
||||
ArrowLeftRightIcon,
|
||||
BlocksIcon,
|
||||
BoxIcon,
|
||||
BracesIcon,
|
||||
CreditCardIcon,
|
||||
GlobeIcon,
|
||||
LockIcon,
|
||||
PanelTopIcon,
|
||||
PencilIcon,
|
||||
PlaneIcon,
|
||||
RectangleHorizontalIcon,
|
||||
RssIcon,
|
||||
ScanTextIcon,
|
||||
ShieldIcon,
|
||||
ShoppingBagIcon,
|
||||
SquircleDashedIcon,
|
||||
StampIcon,
|
||||
UserIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { InsightIcon } from "../icons/InsightIcon";
|
||||
import { ThirdwebIcon } from "../icons/ThirdwebMiniLogo";
|
||||
|
||||
export default function Page() {
|
||||
redirect("/wallets/sign-in/button");
|
||||
return (
|
||||
<main className="pb-10">
|
||||
{/* Hero Section */}
|
||||
<section className="py-16">
|
||||
<div className="container max-w-5xl">
|
||||
<div className="flex mb-6">
|
||||
<div className="rounded-full p-3 border bg-card">
|
||||
<ThirdwebIcon
|
||||
isMonoChrome
|
||||
className="size-9 text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 className="mb-3 font-bold text-5xl tracking-tighter">
|
||||
thirdweb Playground
|
||||
</h1>
|
||||
<p className="max-w-4xl text-base md:text-lg text-muted-foreground leading-normal">
|
||||
Interactive UI components and endpoints to test, tweak, and ship
|
||||
faster with thirdweb.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="container max-w-5xl space-y-12">
|
||||
{/* Wallets Section */}
|
||||
<section>
|
||||
<SectionTitle label="Wallets" />
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<FeatureCard
|
||||
icon={RectangleHorizontalIcon}
|
||||
title="Connect Button"
|
||||
href="/wallets/sign-in/button"
|
||||
description="Wallet connection component for EOA or email, mobile, social, and passkey logins"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={PanelTopIcon}
|
||||
title="Connect Embed"
|
||||
href="/wallets/sign-in/embed"
|
||||
description="Embedded component to view balance, get funds, and more"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={SquircleDashedIcon}
|
||||
title="Headless Connect"
|
||||
href="/wallets/sign-in/headless"
|
||||
description="Customizable wallet connection components using React hooks"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={UserIcon}
|
||||
title="In-App Wallets"
|
||||
href="/wallets/in-app-wallet"
|
||||
description="Add social login, passkey, phone, or email sign-in to your app"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={LockIcon}
|
||||
title="Authentication (SIWE)"
|
||||
href="/wallets/auth"
|
||||
description="Authenticate users to your backend using their wallet"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={GlobeIcon}
|
||||
title="Social Profiles"
|
||||
href="/wallets/social"
|
||||
description="Get user profiles across apps like ENS, Lens, Farcaster, and more"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Transactions Section */}
|
||||
<section>
|
||||
<SectionTitle label="Transactions" />
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<FeatureCard
|
||||
icon={PlaneIcon}
|
||||
title="Airdrop Tokens"
|
||||
href="/transactions/airdrop-tokens"
|
||||
description="Airdrop any token with a few lines of code with gas sponsorship, optional."
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={StampIcon}
|
||||
title="Mint NFTs"
|
||||
href="/transactions/mint-tokens"
|
||||
description="Gasless and efficient token minting with just a wallet address"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={RssIcon}
|
||||
href="/transactions/webhooks"
|
||||
title="Webhooks"
|
||||
description="Receive real-time notifications for transactions and wallet events."
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Contracts Section */}
|
||||
<section>
|
||||
<SectionTitle label="Contracts" />
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<FeatureCard
|
||||
icon={ScanTextIcon}
|
||||
title="Read Contract"
|
||||
href="/contracts/read"
|
||||
description="Read data from any contract on any EVM"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={PencilIcon}
|
||||
title="Write Contract"
|
||||
href="/contracts/write"
|
||||
description="Send transactions from the connected wallet"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={BlocksIcon}
|
||||
title="Pre-Built Extensions"
|
||||
href="/contracts/extensions"
|
||||
description="High-level read and write functions"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={RssIcon}
|
||||
title="Listen Contract Events"
|
||||
href="/contracts/events"
|
||||
description="Subscribe to any contract event"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Payments Section */}
|
||||
<section>
|
||||
<SectionTitle label="Payments" />
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<FeatureCard
|
||||
icon={BoxIcon}
|
||||
title="Payments UI Components"
|
||||
description="Onramp, swap, and bridge over 1,000+ crypto tokens"
|
||||
href="/payments/ui-components"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={ShoppingBagIcon}
|
||||
title="Buy Crypto"
|
||||
description="Buy any token with ability to customize theme, amounts, and more"
|
||||
href="/payments/fund-wallet"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={CreditCardIcon}
|
||||
title="Checkout"
|
||||
description="Enable crypto payments for services and get notified on each sale"
|
||||
href="/payments/commerce"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={ArrowLeftRightIcon}
|
||||
title="Transactions"
|
||||
description="Enable users to pay for onchain transactions with fiat or crypto"
|
||||
href="/payments/transactions"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={BracesIcon}
|
||||
title="Payments API"
|
||||
description="Create customizable UIs or backend flows using the HTTP API"
|
||||
href="/payments/backend"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Insight Section */}
|
||||
<section>
|
||||
<SectionTitle label="Insight" />
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<FeatureCard
|
||||
icon={InsightIcon}
|
||||
title="Events"
|
||||
href="/insight"
|
||||
description="Query contract events on any supported EVM chain"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={InsightIcon}
|
||||
title="Transactions"
|
||||
href="/insight"
|
||||
description="Query transactions to and from specified recipients"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={InsightIcon}
|
||||
title="Tokens"
|
||||
href="/insight"
|
||||
description="Query token owners, transfers, prices, and more"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={InsightIcon}
|
||||
title="NFTs"
|
||||
href="/insight"
|
||||
description="Query NFT balances, collections, transfers, metadata, and more."
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={InsightIcon}
|
||||
title="Wallets"
|
||||
href="/insight"
|
||||
description="Query transactions to and from specific wallets"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={InsightIcon}
|
||||
title="Contracts"
|
||||
href="/insight"
|
||||
description="Query a contract's ABI or metadata"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Account Abstraction Section */}
|
||||
<section>
|
||||
<SectionTitle label="Account Abstraction" />
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<FeatureCard
|
||||
icon={ShieldIcon}
|
||||
title="EIP-4337"
|
||||
href="/account-abstraction/eip-4337"
|
||||
description="Implement via a higher-layer mempool using objects and bundlers"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={ShieldIcon}
|
||||
title="EIP-7702"
|
||||
href="/account-abstraction/eip-7702"
|
||||
description="Allow EOAs to temporarily behave like smart contracts during txs"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={ShieldIcon}
|
||||
title="EIP-5792"
|
||||
href="/account-abstraction/eip-5792"
|
||||
description="Define a standard RPC interface for smart account interactions"
|
||||
/>
|
||||
<FeatureCard
|
||||
icon={ShieldIcon}
|
||||
title="Native (zkSync)"
|
||||
href="/account-abstraction/native-aa"
|
||||
description="Native account abstraction available for zkSync chains"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionTitle(props: { label: string }) {
|
||||
return (
|
||||
<h2 className="font-semibold text-xl text-foreground tracking-tight mb-3">
|
||||
{props.label}
|
||||
</h2>
|
||||
);
|
||||
}
|
||||
|
||||
function FeatureCard(props: {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: React.FC<{ className?: string }>;
|
||||
href: string; // todo make this required
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-4 hover:border-active-border relative">
|
||||
<div className="flex mb-4">
|
||||
<div className="p-2 rounded-full border bg-background">
|
||||
<props.icon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="font-medium mb-0.5 text-lg tracking-tight">
|
||||
<Link className="before:absolute before:inset-0" href={props.href}>
|
||||
{props.title}
|
||||
</Link>
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">{props.description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,32 @@
|
||||
import { BracesIcon } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { PageHeader } from "../../../components/blocks/APIHeader";
|
||||
import { PageHeader } from "@/components/blocks/APIHeader";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
|
||||
const title = "Payments API";
|
||||
const description =
|
||||
"Create customizable components or backend flows with an HTTP API to onramp, swap, and bridge to and from different cryptocurrencies";
|
||||
const ogDescription =
|
||||
"Bridge, swap, and onramp any currency with thirdweb’s Payments REST API. Use simple HTTP requests to integrate from your backend.";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description: ogDescription,
|
||||
title,
|
||||
image: {
|
||||
icon: "payments",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Layout(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
description={
|
||||
<>HTTP API to bridge, swap and onramp to and from any currency</>
|
||||
}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/payments?utm_source=playground"
|
||||
title="Payments API"
|
||||
icon={BracesIcon}
|
||||
title={title}
|
||||
containerClassName="max-w-[1400px]"
|
||||
/>
|
||||
|
||||
{props.children}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ArrowUpRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
|
||||
@@ -7,10 +8,10 @@ export default async function Page() {
|
||||
try {
|
||||
const paths = await getBridgePaths();
|
||||
return (
|
||||
<div className="container pb-20">
|
||||
<div className="container max-w-[1400px] pb-20">
|
||||
<div className="flex flex-col justify-between md:flex-row md:gap-8">
|
||||
<div>
|
||||
<h2 className="mb-1 font-semibold text-2xl tracking-tight">
|
||||
<h2 className="mb-0.5 font-semibold text-xl tracking-tight">
|
||||
thirdweb Payments REST API
|
||||
</h2>
|
||||
<p className="mb-5 text-muted-foreground">
|
||||
@@ -20,8 +21,13 @@ export default async function Page() {
|
||||
</div>
|
||||
|
||||
<Link href="https://bridge.thirdweb.com/reference" target="_blank">
|
||||
<Button className="max-md:w-full" variant="outline">
|
||||
<Button
|
||||
className="max-md:w-full rounded-full gap-2"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
View all endpoints
|
||||
<ArrowUpRightIcon className="size-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@ export default async function Page(props: {
|
||||
|
||||
const title = pathMetadata.summary || "";
|
||||
return (
|
||||
<div className="container pb-8">
|
||||
<div className="container max-w-[1400px] pb-8">
|
||||
<Breadcrumbs />
|
||||
<div className="h-3" />
|
||||
{title && (
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
import type { Metadata } from "next";
|
||||
import { CreditCardIcon } from "lucide-react";
|
||||
import { PageLayout } from "@/components/blocks/APIHeader";
|
||||
import { CodeExample } from "@/components/code/code-example";
|
||||
import { BuyMerchPreview } from "@/components/pay/direct-payment";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description:
|
||||
"The easiest way for users to transact in your app. Onramp users in clicks and generate revenue for each user transaction. Integrate for free.",
|
||||
metadataBase,
|
||||
title: "Integrate Fiat & Cross-Chain Crypto Payments | thirdweb Payments",
|
||||
};
|
||||
const title = "Checkout Component";
|
||||
const description =
|
||||
"Enable purchase of any service or goods with fiat or cryptocurrency and setup notifications on every sale to ship goods, activate services, and more";
|
||||
const ogDescription =
|
||||
"Accept fiat or crypto payments on any chain—direct to your wallet. Instant checkout, webhook support, and full control over post-sale actions.";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description: ogDescription,
|
||||
title,
|
||||
image: {
|
||||
icon: "payments",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>
|
||||
Let your users pay for any service with fiat or crypto on any chain.
|
||||
</>
|
||||
}
|
||||
icon={CreditCardIcon}
|
||||
title={title}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/payments?utm_source=playground"
|
||||
title="Commerce payments with fiat or crypto"
|
||||
>
|
||||
<BuyMerch />
|
||||
</PageLayout>
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ShoppingBagIcon } from "lucide-react";
|
||||
import { PageLayout } from "@/components/blocks/APIHeader";
|
||||
import { CodeExample } from "@/components/code/code-example";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { StyledBuyWidgetPreview } from "@/components/universal-bridge/buy";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description:
|
||||
"The easiest way for users to fund their wallets. Onramp users in clicks and generate revenue for each user transaction. Integrate for free.",
|
||||
metadataBase,
|
||||
title: "Buy Crypto | thirdweb Payments",
|
||||
};
|
||||
const title = "Buy Crypto Component";
|
||||
const description =
|
||||
"Embeddable component for users to purchase any cryptocurrency for top-ups and more with fiat or crypto-to-crypto swaps";
|
||||
const ogDescription =
|
||||
"Configure a component to buy cryptocurrency with specified amounts, customization, and more. This interactive playground shows how to customize the component.";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description: ogDescription,
|
||||
title,
|
||||
image: {
|
||||
icon: "payments",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>
|
||||
Onramp users with credit card & cross-chain crypto payments —
|
||||
and generate revenue for each user transaction.
|
||||
</>
|
||||
}
|
||||
icon={ShoppingBagIcon}
|
||||
title={title}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/wallets/sponsor-gas?utm_source=playground"
|
||||
title="The easiest way for users to fund their wallets"
|
||||
>
|
||||
<StyledPayWidget />
|
||||
</PageLayout>
|
||||
|
||||
|
Before Width: | Height: | Size: 388 KiB |
@@ -1,4 +1,4 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ArrowLeftRightIcon } from "lucide-react";
|
||||
import { PageLayout } from "@/components/blocks/APIHeader";
|
||||
import { CodeExample } from "@/components/code/code-example";
|
||||
import {
|
||||
@@ -6,28 +6,32 @@ import {
|
||||
PayTransactionPreview,
|
||||
} from "@/components/pay/transaction-button";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description:
|
||||
"The easiest way for users to transact in your app. Onramp users in clicks and generate revenue for each user transaction. Integrate for free.",
|
||||
metadataBase,
|
||||
title: "Integrate Fiat & Cross-Chain Crypto Payments | thirdweb Pay",
|
||||
};
|
||||
const title = "Onchain Transaction Component";
|
||||
const description =
|
||||
"Enable seamless onchain transactions for any contract with fiat or crypto with amounts calculated and automatic execution after funds are confirmed.";
|
||||
const ogDescription =
|
||||
"Power onchain transactions with fiat or crypto payments. Automatically calculate costs and run the transaction post onramp or token swap.";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description: ogDescription,
|
||||
title,
|
||||
image: {
|
||||
icon: "payments",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
icon={ArrowLeftRightIcon}
|
||||
containerClassName="space-y-12"
|
||||
description={
|
||||
<>
|
||||
Let your users pay for onchain transactions with fiat or crypto on
|
||||
any chain.
|
||||
</>
|
||||
}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/wallets/sponsor-gas?utm_source=playground"
|
||||
title="Onchain transactions with fiat or crypto"
|
||||
title={title}
|
||||
>
|
||||
<BuyOnchainAsset />
|
||||
<NoFundsPopup />
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import type { Metadata } from "next";
|
||||
import { BoxIcon } from "lucide-react";
|
||||
import { PageLayout } from "@/components/blocks/APIHeader";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import { PageLayout } from "../../../components/blocks/APIHeader";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
import PayEmbedPlayground from "../embed/page";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description:
|
||||
"The easiest way for users to transact in your app. Onramp users, pay with any token and generate revenue for each user transaction. Integrate for free.",
|
||||
metadataBase,
|
||||
title: "Integrate Fiat & Cross-Chain Crypto Payments | thirdweb Payments",
|
||||
};
|
||||
const title = "Crypto Payments UI Components";
|
||||
const description =
|
||||
"Onramp, swap, & bridge over 1,000+ tokens to enable seamless crypto payments, checkouts, and transactions";
|
||||
const ogDescription =
|
||||
"Onramp, swap, and bridge cryptocurrency with easy to implement components for purchasing crypto, checking out physical or digital goods and services, and executing onchain transactions.";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description: ogDescription,
|
||||
title,
|
||||
image: {
|
||||
icon: "payments",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page(props: {
|
||||
searchParams: Promise<{ tab: string }>;
|
||||
@@ -17,14 +25,10 @@ export default function Page(props: {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>
|
||||
Onramp users with credit card & cross-chain crypto payments —
|
||||
and generate revenue for each user transaction.
|
||||
</>
|
||||
}
|
||||
icon={BoxIcon}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/payments?utm_source=playground"
|
||||
title="Payments UI component"
|
||||
title={title}
|
||||
>
|
||||
<PayEmbedPlayground searchParams={props.searchParams} />
|
||||
</PageLayout>
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { arbitrum, base, ethereum } from "thirdweb/chains";
|
||||
import { PageLayout } from "@/components/blocks/APIHeader";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { TokenSelector } from "@/components/ui/TokenSelector";
|
||||
import { THIRDWEB_CLIENT } from "@/lib/client";
|
||||
import type { TokenMetadata } from "@/lib/types";
|
||||
|
||||
export default function TokenSelectorDemo() {
|
||||
const [selectedToken, setSelectedToken] = useState<
|
||||
{ chainId: number; address: string } | undefined
|
||||
>(undefined);
|
||||
|
||||
const [selectedChain, setSelectedChain] = useState<number>(ethereum.id);
|
||||
|
||||
const chains = [
|
||||
{ id: ethereum.id, name: "Ethereum" },
|
||||
{ id: base.id, name: "Base" },
|
||||
{ id: arbitrum.id, name: "Arbitrum" },
|
||||
];
|
||||
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description="Demo of the TokenSelector component ported from dashboard"
|
||||
docsLink="https://portal.thirdweb.com/react/v5/components/onchain?utm_source=playground"
|
||||
title="Token Selector Demo"
|
||||
>
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold">Select a Chain</h2>
|
||||
<select
|
||||
className="rounded border border-border bg-background p-2"
|
||||
onChange={(e) => {
|
||||
setSelectedChain(Number(e.target.value));
|
||||
setSelectedToken(undefined); // Reset token when chain changes
|
||||
}}
|
||||
value={selectedChain}
|
||||
>
|
||||
{chains.map((chain) => (
|
||||
<option key={chain.id} value={chain.id}>
|
||||
{chain.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold">Select a Token</h2>
|
||||
<div className="max-w-md">
|
||||
<TokenSelector
|
||||
addNativeTokenIfMissing={true}
|
||||
chainId={selectedChain}
|
||||
client={THIRDWEB_CLIENT}
|
||||
enabled={true}
|
||||
onChange={(token: TokenMetadata) => {
|
||||
setSelectedToken({
|
||||
address: token.address,
|
||||
chainId: token.chainId,
|
||||
});
|
||||
}}
|
||||
placeholder="Select a token"
|
||||
selectedToken={selectedToken}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedToken && (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold">Selected Token</h2>
|
||||
<div className="rounded border border-border bg-muted p-4">
|
||||
<p>
|
||||
<strong>Chain ID:</strong> {selectedToken.chainId}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Address:</strong> {selectedToken.address}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PageLayout>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ImageIcon } from "lucide-react";
|
||||
import { PageLayout } from "@/components/blocks/APIHeader";
|
||||
import {
|
||||
NftCardExample,
|
||||
@@ -7,24 +7,29 @@ import {
|
||||
NftNameExample,
|
||||
} from "@/components/headless-ui/nft-examples";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description:
|
||||
"Elevate your NFT marketplace with our React headless UI components, engineered for seamless digital asset transactions. These customizable, zero-styling components simplify NFT interactions while giving developers complete freedom to craft their perfect user interface.",
|
||||
metadataBase,
|
||||
title: "NFT Components",
|
||||
};
|
||||
const title = "NFT Components";
|
||||
const description =
|
||||
"Headless UI components for rendering NFT Media and metadata";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title,
|
||||
description,
|
||||
image: {
|
||||
icon: "wallets",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>Headless UI components for rendering NFT Media and metadata</>
|
||||
}
|
||||
icon={ImageIcon}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/react/v5/components/onchain#nfts?utm_source=playground"
|
||||
title="NFT Components"
|
||||
title={title}
|
||||
>
|
||||
<div className="space-y-14">
|
||||
<NftMediaExample />
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Metadata } from "next";
|
||||
import { DollarSignIcon } from "lucide-react";
|
||||
import { PageLayout } from "@/components/blocks/APIHeader";
|
||||
import {
|
||||
TokenImageBasic,
|
||||
@@ -6,27 +6,30 @@ import {
|
||||
TokenSymbolBasic,
|
||||
} from "@/components/headless-ui/token-examples";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description:
|
||||
"Headless UI components for rendering token image, name, and symbol",
|
||||
metadataBase,
|
||||
title: "Token Components",
|
||||
};
|
||||
const title = "Token Components";
|
||||
const description =
|
||||
"Headless UI components for rendering token image, name, and symbol";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title,
|
||||
description,
|
||||
image: {
|
||||
icon: "wallets",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
containerClassName="space-y-12"
|
||||
description={
|
||||
<>
|
||||
Headless UI components for rendering token image, name, and symbol
|
||||
</>
|
||||
}
|
||||
icon={DollarSignIcon}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/react/v5/components/onchain#tokens?utm_source=playground"
|
||||
title="Token Components"
|
||||
title={title}
|
||||
>
|
||||
<TokenImageBasic />
|
||||
<TokenNameBasic />
|
||||
@@ -1,20 +1,33 @@
|
||||
import { PlaneIcon } from "lucide-react";
|
||||
import { EngineAirdropPreview } from "@/app/transactions/airdrop-tokens/_components/airdrop-preview";
|
||||
import { PageLayout } from "@/components/blocks/APIHeader";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { PageLayout } from "../../../components/blocks/APIHeader";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
import { AirdropCode } from "./_components/airdrop-code";
|
||||
|
||||
const title = "Airdrop Tokens";
|
||||
const description =
|
||||
"Airdrop ERC-20, ERC-721, or ERC-1155 tokens at scale to multiple addresses. Support gas sponsorship and receive real-time status updates for each transaction.";
|
||||
const ogDescription =
|
||||
"Airdrop any collection of ERC20, ERC721, or ERC1155 tokens with a few lines of code. Try the flow, inspect the smart contract, and copy code directly into your app.";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description: ogDescription,
|
||||
title,
|
||||
image: {
|
||||
icon: "transactions",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>
|
||||
Engine makes it effortless for any developer to airdrop tokens at
|
||||
scale. You sponsor the gas so your users only need a wallet address!
|
||||
</>
|
||||
}
|
||||
icon={PlaneIcon}
|
||||
description={description}
|
||||
docsLink="https://thirdweb-engine.apidocumentation.com/reference#tag/erc20/POST/contract/{chain}/{contractAddress}/erc20/mint-batch-to?utm_source=playground"
|
||||
title="Airdrop"
|
||||
title={title}
|
||||
>
|
||||
<EngineAirdropPreview />
|
||||
<div className="h-10" />
|
||||
|
||||
@@ -1,20 +1,33 @@
|
||||
import { StampIcon } from "lucide-react";
|
||||
import { EngineMintPreview } from "@/app/transactions/mint-tokens/_components/mint-preview";
|
||||
import { PageLayout } from "@/components/blocks/APIHeader";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { PageLayout } from "../../../components/blocks/APIHeader";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
import { MintCode } from "./_components/mint-code";
|
||||
|
||||
const title = "Mint NFTs";
|
||||
const description =
|
||||
"Enable users to mint new tokens into any smart contract. Gas fees are sponsored, so users only need to provide a wallet address";
|
||||
const ogDescription =
|
||||
"Interactive demo for gasless token minting. Mint tokens to any contract with just a wallet address. Sponsor gas and streamline UX.";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description: ogDescription,
|
||||
title,
|
||||
image: {
|
||||
icon: "transactions",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>
|
||||
Allow your users to mint new tokens into any given contract. You
|
||||
sponsor the gas so your users only need a wallet address!
|
||||
</>
|
||||
}
|
||||
icon={StampIcon}
|
||||
description={description}
|
||||
docsLink="https://thirdweb-engine.apidocumentation.com/reference#tag/erc1155/POST/contract/{chain}/{contractAddress}/erc1155/mint-to?utm_source=playground"
|
||||
title="Mint Dynamic NFTs"
|
||||
title={title}
|
||||
>
|
||||
<EngineMintPreview />
|
||||
<div className="h-10" />
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
import { RssIcon } from "lucide-react";
|
||||
import { EngineWebhooksPreview } from "@/app/transactions/webhooks/_components/webhooks-preview";
|
||||
import { PageLayout } from "@/components/blocks/APIHeader";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { PageLayout } from "../../../components/blocks/APIHeader";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
|
||||
const title = "Transaction Webhooks";
|
||||
const description =
|
||||
"Configure webhooks to notify your backend of transaction events or activity from your server wallet";
|
||||
const ogDescription =
|
||||
"Set up webhooks using Transactions to receive real-time notifications for transactions and wallet events. Test the flow and see example payloads in action.";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description: ogDescription,
|
||||
title,
|
||||
image: {
|
||||
icon: "transactions",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>
|
||||
Configure webhooks in Engine to notify your backend server of
|
||||
transaction or backend wallet events.
|
||||
</>
|
||||
}
|
||||
icon={RssIcon}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/engine/v2/features/webhooks?utm_source=playground"
|
||||
title="Webhooks"
|
||||
title={title}
|
||||
>
|
||||
<EngineWebhooksPreview />
|
||||
</PageLayout>
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import {
|
||||
ConnectSmartAccountCustomPreview,
|
||||
ConnectSmartAccountPreview,
|
||||
} from "../../../../components/account-abstraction/connect-smart-account";
|
||||
import { PageLayout } from "../../../../components/blocks/APIHeader";
|
||||
import { CodeExample } from "../../../../components/code/code-example";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description:
|
||||
"Let users sign up with their email, phone number, social media accounts or directly with a wallet. Seamlessly integrate account abstraction and SIWE auth.",
|
||||
metadataBase,
|
||||
title: "Account Abstraction | thirdweb Connect",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>
|
||||
Let users connect to their smart accounts with any wallet and unlock
|
||||
gas sponsorship, batched transactions, session keys and full wallet
|
||||
programmability.
|
||||
</>
|
||||
}
|
||||
docsLink="https://portal.thirdweb.com/wallets/sponsor-gas?utm_source=playground"
|
||||
title="Connect smart accounts"
|
||||
>
|
||||
<ConnectSmartAccount />
|
||||
</PageLayout>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ConnectSmartAccount() {
|
||||
return (
|
||||
<>
|
||||
<CodeExample
|
||||
code={`\
|
||||
import { ConnectButton } from "thirdweb/react";
|
||||
|
||||
function App(){
|
||||
return (<>
|
||||
<ConnectButton client={client}
|
||||
// account abstraction options
|
||||
accountAbstraction={{ chain, sponsorGas: true }} />
|
||||
</>);
|
||||
};`}
|
||||
header={{
|
||||
description:
|
||||
"Use the prebuilt UI components to connect to smart accounts",
|
||||
title: "Using prebuilt UI component",
|
||||
}}
|
||||
lang="tsx"
|
||||
preview={<ConnectSmartAccountPreview />}
|
||||
/>
|
||||
|
||||
<div className="h-14" />
|
||||
<CodeExample
|
||||
code={`\
|
||||
import { useConnect } from "thirdweb/react";
|
||||
import { createWallet } from "thirdweb/wallets";
|
||||
|
||||
function App(){
|
||||
const { connect } = useConnect({ client,
|
||||
// account abstraction options
|
||||
accountAbstraction: { chain, sponsorGas: true }});
|
||||
|
||||
return (<>
|
||||
<button onClick={() => connect(async () => {
|
||||
// any wallet connected here will be
|
||||
// converted to a smart account
|
||||
const adminWallet = inAppWallet();
|
||||
await adminWallet.connect({ client, strategy: "google" });
|
||||
return adminWallet;
|
||||
})}>Connect with Google</button>
|
||||
</>);
|
||||
};`}
|
||||
header={{
|
||||
description: "Build your own UI to connect to smart accounts",
|
||||
title: "Build custom UI",
|
||||
}}
|
||||
lang="tsx"
|
||||
preview={<ConnectSmartAccountCustomPreview />}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +1,34 @@
|
||||
import type { Metadata } from "next";
|
||||
import { LockIcon } from "lucide-react";
|
||||
import { BasicAuthPreview } from "@/components/auth/basic-auth";
|
||||
import { GatedContentPreview } from "@/components/auth/gated-content";
|
||||
import { SmartAccountAuthPreview } from "@/components/auth/smart-account-auth";
|
||||
import { CodeExample } from "@/components/code/code-example";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import { BasicAuthHookPreview } from "../../../components/auth/basic-auth-hook";
|
||||
import { PageLayout } from "../../../components/blocks/APIHeader";
|
||||
import { createMetadata } from "../../../lib/metadata";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description:
|
||||
"Authenticate users to your backend using only their wallet. This is a secure and easy way to authenticate users without requiring them to create an additional account.",
|
||||
metadataBase,
|
||||
title: "Auth | thirdweb Connect",
|
||||
};
|
||||
const title = "Authentication (SIWE)";
|
||||
const description =
|
||||
"Add secure wallet authentication to your app using SIWE and JWT. Authenticate users without passwords via Sign-In with Ethereum and backend token validation";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description,
|
||||
title,
|
||||
image: {
|
||||
icon: "wallets",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>
|
||||
Authenticate users to your backend using only their wallet. This is
|
||||
a secure and easy way to authenticate users without requiring them
|
||||
to create an additional account.
|
||||
</>
|
||||
}
|
||||
icon={LockIcon}
|
||||
title={title}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/typescript/v5/auth?utm_source=playground"
|
||||
title="Auth"
|
||||
>
|
||||
<div className="flex flex-col gap-14">
|
||||
<BasicAuth />
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ReadContractExtensionPreview } from "@/components/blockchain-api/read-contract-extension";
|
||||
import { ReadContractRawPreview } from "@/components/blockchain-api/read-contract-raw";
|
||||
import { WatchEventPreview } from "@/components/blockchain-api/watch-event-preview";
|
||||
import { WriteContractExtensionPreview } from "@/components/blockchain-api/write-contract-extension";
|
||||
import { WriteContractRawPreview } from "@/components/blockchain-api/write-contract-raw";
|
||||
import { CodeExample } from "@/components/code/code-example";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import { PageLayout } from "../../../components/blocks/APIHeader";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description:
|
||||
"Interact with EVM blockchains using thirdweb SDK. Create seamless NFT minting experience. Airdrop tokens to millions of users",
|
||||
metadataBase,
|
||||
title: "Blockchain API | thirdweb Connect",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>
|
||||
Performant, reliable and type safe API to read write to any contract
|
||||
on any EVM chain through our RPC Edge endpoints.
|
||||
</>
|
||||
}
|
||||
docsLink="https://portal.thirdweb.com/typescript/v5?utm_source=playground"
|
||||
title="Blockchain API"
|
||||
>
|
||||
<div className="flex flex-col gap-14">
|
||||
<ReadContractRaw />
|
||||
<ReadContractExtension />
|
||||
<WriteContractExtension />
|
||||
<WriteContractRaw />
|
||||
|
||||
<WatchEvent />
|
||||
</div>
|
||||
</PageLayout>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ReadContractRaw() {
|
||||
return (
|
||||
<CodeExample
|
||||
code={`import { getContract } from "thirdweb";
|
||||
import { ethereum } from "thirdweb/chains";
|
||||
import { MediaRenderer, useReadContract } from "thirdweb/react";
|
||||
|
||||
const onChainCryptoPunks = getContract({
|
||||
address: "0x16F5A35647D6F03D5D3da7b35409D65ba03aF3B2",
|
||||
chain: ethereum,
|
||||
client: THIRDWEB_CLIENT,
|
||||
});
|
||||
|
||||
function App() {
|
||||
// Read the image of the tokenId #1
|
||||
const { data } = useReadContract({
|
||||
contract: onChainCryptoPunks,
|
||||
method: "function punkImageSvg(uint16 index) view returns (string svg)",
|
||||
params: [1],
|
||||
});
|
||||
|
||||
return (
|
||||
<MediaRenderer
|
||||
client={THIRDWEB_CLIENT}
|
||||
src={data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
`}
|
||||
header={{
|
||||
description:
|
||||
"Read data from any contract or wallet. Type safe functions and hooks without needing full ABIs.",
|
||||
title: "Query blockchain data",
|
||||
}}
|
||||
lang="tsx"
|
||||
preview={<ReadContractRawPreview />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ReadContractExtension() {
|
||||
return (
|
||||
<CodeExample
|
||||
code={`import { getContract } from "thirdweb";
|
||||
import { ethereum } from "thirdweb/chains";
|
||||
import { MediaRenderer, useReadContract } from "thirdweb/react";
|
||||
|
||||
const nftContract = getContract({
|
||||
address: "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d",
|
||||
chain: ethereum,
|
||||
client: THIRDWEB_CLIENT,
|
||||
});
|
||||
|
||||
function App() {
|
||||
const { data } = useReadContract(getNFT, {
|
||||
contract: nftContract,
|
||||
tokenId: 458n,
|
||||
});
|
||||
|
||||
return (
|
||||
<MediaRenderer
|
||||
client={THIRDWEB_CLIENT}
|
||||
src={data?.metadata.image}
|
||||
/>
|
||||
);
|
||||
}
|
||||
`}
|
||||
header={{
|
||||
description:
|
||||
"Extensions let you do more with less code. High level functions with simple API that do pre and post processing for all common standards.",
|
||||
title: "Prebuilt read extensions",
|
||||
}}
|
||||
lang="tsx"
|
||||
preview={<ReadContractExtensionPreview />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function WriteContractExtension() {
|
||||
return (
|
||||
<CodeExample
|
||||
code={`import { getContract } from "thirdweb";
|
||||
import { sepolia } from "thirdweb/chains";
|
||||
import { claimTo } from "thirdweb/extensions/erc20";
|
||||
|
||||
const tw_coin = getContract({
|
||||
address: "0xACf072b740a23D48ECd302C9052fbeb3813b60a6",
|
||||
chain: sepolia,
|
||||
client: THIRDWEB_CLIENT,
|
||||
});
|
||||
|
||||
function App() {
|
||||
return <TransactionButton
|
||||
transaction={() =>
|
||||
claimTo({
|
||||
contract: twCoinContract,
|
||||
to: account.address,
|
||||
quantity: "10",
|
||||
})
|
||||
}
|
||||
>
|
||||
Claim
|
||||
</TransactionButton>
|
||||
}
|
||||
`}
|
||||
header={{
|
||||
description:
|
||||
"Extensions let you do more with less code. High level functions with simple API that do pre and post processing for all common standards.",
|
||||
title: "Prebuilt write extensions",
|
||||
}}
|
||||
lang="tsx"
|
||||
preview={<WriteContractExtensionPreview />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function WriteContractRaw() {
|
||||
return (
|
||||
<CodeExample
|
||||
code={`import { getContract, prepareContractCall, toUnits } from "thirdweb";
|
||||
import { sepolia } from "thirdweb/chains";
|
||||
|
||||
const tw_coin = getContract({
|
||||
address: "0xACf072b740a23D48ECd302C9052fbeb3813b60a6",
|
||||
chain: sepolia,
|
||||
client: THIRDWEB_CLIENT,
|
||||
});
|
||||
|
||||
// Claim some test tokens from the above example!
|
||||
|
||||
function App() {
|
||||
return <TransactionButton
|
||||
transaction={() =>
|
||||
prepareContractCall({
|
||||
contract: tw_coin,
|
||||
method:
|
||||
"function transfer(address to, uint256 value) returns (bool)",
|
||||
params: [
|
||||
"0x...",
|
||||
toUnits("5", 18),
|
||||
],
|
||||
})
|
||||
}
|
||||
>
|
||||
Send
|
||||
</TransactionButton>
|
||||
}
|
||||
`}
|
||||
header={{
|
||||
description:
|
||||
"Send transactions with the connected wallet. Type safe functions and hooks to send contracts call or raw transaction.",
|
||||
title: "Write data to blockchain",
|
||||
}}
|
||||
lang="tsx"
|
||||
preview={<WriteContractRawPreview />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function WatchEvent() {
|
||||
return (
|
||||
<CodeExample
|
||||
code={`import { useContractEvents } from "thirdweb/react";
|
||||
import { getContract } from "thirdweb";
|
||||
import { base } from "thirdweb/chains";
|
||||
import { transferEvent } from "thirdweb/extensions/erc20";
|
||||
|
||||
const usdcContractOnBase = getContract({
|
||||
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
||||
chain: base,
|
||||
client,
|
||||
});
|
||||
|
||||
function App() {
|
||||
// Listen to USDC transfers on Base
|
||||
const contractEvents = useContractEvents({
|
||||
contract: usdcContractOnBase,
|
||||
events: [transferEvent()],
|
||||
blockRange: 100,
|
||||
});
|
||||
|
||||
(contractEvents.data || []).forEach((item) => {
|
||||
const { from, to, value } = item.args;
|
||||
console.log("{from}...{value} USDC...{to}");
|
||||
});
|
||||
}
|
||||
`}
|
||||
header={{
|
||||
description:
|
||||
"Subscribe to any contract event. Auto polling hooks and functions with type safe event extensions for all common standards.",
|
||||
title: "Listen to blockchain events",
|
||||
}}
|
||||
lang="tsx"
|
||||
preview={<WatchEventPreview />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,30 +1,32 @@
|
||||
import type { Metadata } from "next";
|
||||
import { AtomIcon } from "lucide-react";
|
||||
import { PageLayout } from "@/components/blocks/APIHeader";
|
||||
import { CodeExample } from "@/components/code/code-example";
|
||||
import { PageLayout } from "../../../../components/blocks/APIHeader";
|
||||
import { EcosystemConnectEmbed } from "../../../../components/in-app-wallet/ecosystem";
|
||||
import { Profiles } from "../../../../components/in-app-wallet/profile-sections";
|
||||
import ThirdwebProvider from "../../../../components/thirdweb-provider";
|
||||
import { metadataBase } from "../../../../lib/constants";
|
||||
import { EcosystemConnectEmbed } from "@/components/in-app-wallet/ecosystem";
|
||||
import { Profiles } from "@/components/in-app-wallet/profile-sections";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description:
|
||||
"Build a public or permissioned ecosystem by allowing third party apps and games to connect to the same accounts.",
|
||||
metadataBase,
|
||||
title: "Build your own Ecosystem | thirdweb",
|
||||
};
|
||||
const title = "Ecosystem Wallets";
|
||||
const description =
|
||||
"Enable global wallet-based identity across apps and games. Create public or permissioned ecosystems where users keep one account everywhere.";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description: description,
|
||||
title,
|
||||
image: {
|
||||
icon: "wallets",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>
|
||||
Build a public or permissioned ecosystem by allowing third party
|
||||
apps and games to connect to the same accounts.
|
||||
</>
|
||||
}
|
||||
icon={AtomIcon}
|
||||
title={title}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/wallets/ecosystem/set-up?utm_source=playground"
|
||||
title="Build your own Ecosystem"
|
||||
>
|
||||
<ConnectEmbedExample />
|
||||
<div className="h-14" />
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Metadata } from "next";
|
||||
import { CircleUserIcon } from "lucide-react";
|
||||
import { PageLayout } from "@/components/blocks/APIHeader";
|
||||
import {
|
||||
AccountAvatarExample,
|
||||
@@ -7,27 +7,29 @@ import {
|
||||
AccountNameExample,
|
||||
} from "@/components/headless-ui/account-examples";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description:
|
||||
"Headless components for rendering account information like ENS name, ENS avatar, account balance and more",
|
||||
metadataBase,
|
||||
title: "Account Components",
|
||||
};
|
||||
const title = "Account Components";
|
||||
const description =
|
||||
"Headless components for rendering account information like ENS name, ENS avatar, account balance and more";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title,
|
||||
description,
|
||||
image: {
|
||||
icon: "wallets",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>
|
||||
Headless components for rendering account information like ENS name,
|
||||
ENS avatar, account balance and more
|
||||
</>
|
||||
}
|
||||
icon={CircleUserIcon}
|
||||
title={title}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/react/v5/components/account?utm_source=playground"
|
||||
title="Account Components"
|
||||
>
|
||||
<div className="space-y-16">
|
||||
<AccountNameExample />
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
import type { Metadata } from "next";
|
||||
import { LinkIcon } from "lucide-react";
|
||||
import { PageLayout } from "@/components/blocks/APIHeader";
|
||||
import {
|
||||
ChainIconExample,
|
||||
ChainNameExample,
|
||||
} from "@/components/headless-ui/chain-examples";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description: "Headless UI components for rendering chain name and icon",
|
||||
metadataBase,
|
||||
title: "Chain Components",
|
||||
};
|
||||
const title = "Chain Components";
|
||||
const description = "Headless UI components for rendering chain name and icon";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title,
|
||||
description,
|
||||
image: {
|
||||
icon: "wallets",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
icon={LinkIcon}
|
||||
containerClassName="space-y-12"
|
||||
description={
|
||||
<>Headless UI components for rendering chain name and icon</>
|
||||
}
|
||||
title={title}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/react/v5/components/onchain#chains?utm_source=playground"
|
||||
title="Chain Components"
|
||||
>
|
||||
<ChainIconExample />
|
||||
<ChainNameExample />
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
import type { Metadata } from "next";
|
||||
import { WalletCardsIcon } from "lucide-react";
|
||||
import { PageLayout } from "@/components/blocks/APIHeader";
|
||||
import {
|
||||
WalletIconExample,
|
||||
WalletNameExample,
|
||||
} from "@/components/headless-ui/wallet-examples";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description: "Headless UI components for rendering wallet name and icon",
|
||||
metadataBase,
|
||||
title: "Wallet Components",
|
||||
};
|
||||
const title = "Wallet Components";
|
||||
const description = "Headless UI components for rendering wallet name and icon";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
title,
|
||||
description,
|
||||
image: {
|
||||
icon: "wallets",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
icon={WalletCardsIcon}
|
||||
containerClassName="space-y-12"
|
||||
description={
|
||||
<>Headless UI components for rendering wallet name and icon</>
|
||||
}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/react/v5/connecting-wallets/ui-components?utm_source=playground"
|
||||
title="Wallet Components"
|
||||
title={title}
|
||||
>
|
||||
<WalletIconExample />
|
||||
<WalletNameExample />
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import type { Metadata } from "next";
|
||||
import { UserIcon } from "lucide-react";
|
||||
import { CodeExample } from "@/components/code/code-example";
|
||||
import { CustomLoginForm } from "@/components/in-app-wallet/custom-login-form";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
import { PageLayout } from "../../../components/blocks/APIHeader";
|
||||
import { InAppConnectEmbed } from "../../../components/in-app-wallet/connect-button";
|
||||
import { Profiles } from "../../../components/in-app-wallet/profile-sections";
|
||||
import ThirdwebProvider from "../../../components/thirdweb-provider";
|
||||
import { metadataBase } from "../../../lib/constants";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description:
|
||||
"Let users sign up with their email, phone number, social media accounts or directly with a wallet",
|
||||
metadataBase,
|
||||
title: "Any Auth | thirdweb in-app wallet",
|
||||
};
|
||||
const title = "In-App Wallets";
|
||||
const description =
|
||||
"Add social login, passkey, phone, or email sign-in to your app. Use built-in auth or connect your own backend with custom endpoints.";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description,
|
||||
title,
|
||||
image: {
|
||||
icon: "wallets",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>
|
||||
Use any of the built-in auth methods or bring your own.
|
||||
<br />
|
||||
Supports custom auth endpoints to integrate with your existing user
|
||||
base.
|
||||
</>
|
||||
}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/wallets/users?utm_source=playground"
|
||||
title="Onboard users to web3 with any auth method"
|
||||
title={title}
|
||||
icon={UserIcon}
|
||||
>
|
||||
<UIIntegration />
|
||||
<div className="h-14" />
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import { CodeExample } from "@/components/code/code-example";
|
||||
import { PageLayout } from "../../../../components/blocks/APIHeader";
|
||||
import { SponsoredInAppTxPreview } from "../../../../components/in-app-wallet/sponsored-tx";
|
||||
import ThirdwebProvider from "../../../../components/thirdweb-provider";
|
||||
import { metadataBase } from "../../../../lib/constants";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description:
|
||||
"With in-app wallets, users don't need to confirm every transaction. Combine it with smart account flag to cover gas costs for the best UX",
|
||||
metadataBase,
|
||||
title: "Signless Sponsored Transactions | thirdweb in-app wallet",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>
|
||||
With in-app wallets, users {"don't"} need to confirm every
|
||||
transaction.
|
||||
<br />
|
||||
Combine it with smart account flag to cover gas costs for the best
|
||||
UX.
|
||||
</>
|
||||
}
|
||||
docsLink="https://portal.thirdweb.com/wallets/sponsor-gas?utm_source=playground"
|
||||
title="Signless Sponsored Transactions"
|
||||
>
|
||||
<SponsoredInAppTx />
|
||||
</PageLayout>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function SponsoredInAppTx() {
|
||||
return (
|
||||
<CodeExample
|
||||
code={`
|
||||
import { inAppWallet } from "thirdweb/wallets";
|
||||
import { claimTo } from "thirdweb/extensions/erc1155";
|
||||
import {
|
||||
ConnectButton,
|
||||
TransactionButton,
|
||||
} from "thirdweb/react";
|
||||
import { baseSepolia } from "thirdweb/chains";
|
||||
|
||||
const wallets = [
|
||||
inAppWallet(
|
||||
{
|
||||
// turn on gas sponsorship for in-app wallets
|
||||
// Can use EIP4337 or EIP7702 on supported chains
|
||||
executionMode: {
|
||||
mode: "EIP4337",
|
||||
smartAccount: { chain: baseSepolia, sponsorGas: true },
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<ConnectButton client={client} wallets={wallets} />
|
||||
|
||||
{/* signless, sponsored transactions */}
|
||||
<TransactionButton
|
||||
transaction={() =>
|
||||
claimTo({
|
||||
contract,
|
||||
to: "0x123...",
|
||||
tokenId: 0n,
|
||||
quantity: 1n,
|
||||
})
|
||||
}
|
||||
>
|
||||
Mint
|
||||
</TransactionButton>
|
||||
</>
|
||||
);
|
||||
}`}
|
||||
lang="tsx"
|
||||
preview={<SponsoredInAppTxPreview />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -151,7 +151,7 @@ export function RightSection(props: {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex shrink-0 flex-col gap-4 xl:sticky xl:top-0 xl:max-h-[100vh] xl:w-[764px]">
|
||||
<div className="flex shrink-0 flex-col gap-4 xl:sticky xl:top-0 xl:max-h-[100vh] xl:w-[734px]">
|
||||
<TabButtons
|
||||
tabs={[
|
||||
{
|
||||
@@ -283,11 +283,11 @@ function TabButtons(props: {
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-start gap-1 rounded-lg border bg-card p-2 shadow-md md:inline-flex">
|
||||
<div className="flex justify-start gap-1 rounded-lg border bg-card p-1 md:inline-flex">
|
||||
{props.tabs.map((tab) => (
|
||||
<Button
|
||||
className={cn(
|
||||
"gap-2 px-4 text-base",
|
||||
"gap-2 px-4",
|
||||
tab.isActive
|
||||
? "bg-accent text-foreground"
|
||||
: "bg-transparent text-muted-foreground",
|
||||
@@ -295,6 +295,7 @@ function TabButtons(props: {
|
||||
key={tab.name}
|
||||
onClick={tab.onClick}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
{tab.name}
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import { RectangleHorizontalIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { PageLayout } from "../../../../components/blocks/APIHeader";
|
||||
import type { ConnectPlaygroundOptions } from "../components/types";
|
||||
import { LeftSection } from "./LeftSection";
|
||||
import { RightSection } from "./RightSection";
|
||||
|
||||
const defaultInAppLoginMethods: ConnectPlaygroundOptions["inAppWallet"]["methods"] =
|
||||
[
|
||||
"google",
|
||||
"discord",
|
||||
"telegram",
|
||||
"farcaster",
|
||||
"email",
|
||||
"x",
|
||||
"passkey",
|
||||
"phone",
|
||||
];
|
||||
|
||||
// NOTE: Only set the values that are actually the default values used by Connect component
|
||||
const defaultConnectOptions: ConnectPlaygroundOptions = {
|
||||
buttonLabel: undefined,
|
||||
enableAccountAbstraction: false,
|
||||
enableAuth: false,
|
||||
inAppWallet: {
|
||||
enabled: true,
|
||||
methods: defaultInAppLoginMethods,
|
||||
},
|
||||
localeId: "en_US",
|
||||
modalSize: "compact",
|
||||
modalTitle: undefined,
|
||||
modalTitleIcon: undefined,
|
||||
privacyPolicyLink: undefined,
|
||||
requireApproval: false,
|
||||
ShowThirdwebBranding: true,
|
||||
termsOfServiceLink: undefined,
|
||||
theme: {
|
||||
darkColorOverrides: {},
|
||||
lightColorOverrides: {},
|
||||
type: "dark",
|
||||
},
|
||||
walletIds: [
|
||||
"io.metamask",
|
||||
"com.coinbase.wallet",
|
||||
"me.rainbow",
|
||||
"io.rabby",
|
||||
"io.zerion.wallet",
|
||||
],
|
||||
};
|
||||
|
||||
export function ConnectButtonPage(props: {
|
||||
title: string;
|
||||
description: string;
|
||||
tab: string;
|
||||
}) {
|
||||
const { theme } = useTheme();
|
||||
const [connectOptions, setConnectOptions] =
|
||||
useState<ConnectPlaygroundOptions>(() => ({
|
||||
...defaultConnectOptions,
|
||||
theme: {
|
||||
...defaultConnectOptions.theme,
|
||||
type: theme === "dark" ? "dark" : "light",
|
||||
},
|
||||
}));
|
||||
|
||||
// change theme on global theme change
|
||||
useEffect(() => {
|
||||
setConnectOptions((prev) => ({
|
||||
...prev,
|
||||
theme: {
|
||||
...prev.theme,
|
||||
type: theme === "dark" ? "dark" : "light",
|
||||
},
|
||||
}));
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
icon={RectangleHorizontalIcon}
|
||||
description={props.description}
|
||||
docsLink="https://portal.thirdweb.com/wallets/auth?utm_source=playground"
|
||||
title={props.title}
|
||||
>
|
||||
<div className="relative flex flex-col-reverse gap-6 xl:min-h-[900px] xl:flex-row xl:gap-6">
|
||||
<div className="grow border-b pb-10 xl:mb-0 xl:border-r xl:border-b-0 xl:pr-6">
|
||||
<LeftSection
|
||||
connectOptions={connectOptions}
|
||||
setConnectOptions={setConnectOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<RightSection connectOptions={connectOptions} tab={props.tab} />
|
||||
</div>
|
||||
</PageLayout>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,90 +1,31 @@
|
||||
"use client";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
import { ConnectButtonPage } from "./connect-button-page";
|
||||
|
||||
import { use, useState } from "react";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { PageLayout } from "../../../../components/blocks/APIHeader";
|
||||
import type { ConnectPlaygroundOptions } from "../components/types";
|
||||
import { LeftSection } from "./LeftSection";
|
||||
import { RightSection } from "./RightSection";
|
||||
const title = "Connect Button";
|
||||
const description =
|
||||
"Wallet connection component to enable sign-in to any 500+ EOA (external wallets) or in-app wallets via email, phone number, passkeys, or social logins";
|
||||
const ogDescription =
|
||||
"Plug-and-play wallet connect UI with support for 500+ wallets, passkey and social login, ERC-4337 upgrades, gasless flows, and Sign In with Ethereum.";
|
||||
|
||||
const defaultInAppLoginMethods: ConnectPlaygroundOptions["inAppWallet"]["methods"] =
|
||||
[
|
||||
"google",
|
||||
"discord",
|
||||
"telegram",
|
||||
"farcaster",
|
||||
"email",
|
||||
"x",
|
||||
"passkey",
|
||||
"phone",
|
||||
];
|
||||
|
||||
// NOTE: Only set the values that are actually the default values used by Connect component
|
||||
const defaultConnectOptions: ConnectPlaygroundOptions = {
|
||||
buttonLabel: undefined,
|
||||
enableAccountAbstraction: false,
|
||||
enableAuth: false,
|
||||
inAppWallet: {
|
||||
enabled: true,
|
||||
methods: defaultInAppLoginMethods,
|
||||
export const metadata = createMetadata({
|
||||
description: ogDescription,
|
||||
title,
|
||||
image: {
|
||||
icon: "wallets",
|
||||
title,
|
||||
},
|
||||
localeId: "en_US",
|
||||
modalSize: "compact",
|
||||
modalTitle: undefined,
|
||||
modalTitleIcon: undefined,
|
||||
privacyPolicyLink: undefined,
|
||||
requireApproval: false,
|
||||
ShowThirdwebBranding: true,
|
||||
termsOfServiceLink: undefined,
|
||||
theme: {
|
||||
darkColorOverrides: {},
|
||||
lightColorOverrides: {},
|
||||
type: "dark",
|
||||
},
|
||||
walletIds: [
|
||||
"io.metamask",
|
||||
"com.coinbase.wallet",
|
||||
"me.rainbow",
|
||||
"io.rabby",
|
||||
"io.zerion.wallet",
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
export default function Page(props: {
|
||||
searchParams: Promise<{ tab: string }>;
|
||||
export default async function Page(props: {
|
||||
searchParams: Promise<{ tab: string | undefined | string[] }>;
|
||||
}) {
|
||||
const searchParams = use(props.searchParams);
|
||||
const [connectOptions, setConnectOptions] =
|
||||
useState<ConnectPlaygroundOptions>(defaultConnectOptions);
|
||||
const searchParams = await props.searchParams;
|
||||
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>
|
||||
A fully featured wallet connection component that allows to Connect
|
||||
to 500+ external wallets, connect via email, phone number, passkey
|
||||
or social logins, Convert any wallet to a ERC4337 smart wallet for
|
||||
gasless transactions and provides SIWE (Sign In With Ethereum)
|
||||
</>
|
||||
}
|
||||
docsLink="https://portal.thirdweb.com/wallets/auth?utm_source=playground"
|
||||
title="ConnectButton"
|
||||
>
|
||||
<div className="relative flex flex-col-reverse gap-6 xl:min-h-[900px] xl:flex-row xl:gap-6">
|
||||
<div className="grow border-b pb-10 xl:mb-0 xl:border-r xl:border-b-0 xl:pr-6">
|
||||
<LeftSection
|
||||
connectOptions={connectOptions}
|
||||
setConnectOptions={setConnectOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<RightSection
|
||||
connectOptions={connectOptions}
|
||||
tab={searchParams.tab}
|
||||
/>
|
||||
</div>
|
||||
</PageLayout>
|
||||
</ThirdwebProvider>
|
||||
<ConnectButtonPage
|
||||
title={title}
|
||||
description={description}
|
||||
tab={typeof searchParams.tab === "string" ? searchParams.tab : "modal"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CustomAccordion } from "@/components/ui/CustomAccordion";
|
||||
import { cn } from "../../../../lib/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function CollapsibleSection(props: {
|
||||
children: React.ReactNode;
|
||||
@@ -10,13 +10,13 @@ export function CollapsibleSection(props: {
|
||||
}) {
|
||||
return (
|
||||
<CustomAccordion
|
||||
activeTriggerClassName="bg-card text-foreground "
|
||||
activeTriggerClassName="bg-accent/50 text-foreground"
|
||||
chevronClassName="size-5"
|
||||
chevronPosition="right"
|
||||
containerClassName="border-none"
|
||||
defaultOpen={props.defaultOpen}
|
||||
trigger={
|
||||
<span className="flex items-center gap-3 font-semibold text-lg tracking-tight">
|
||||
<span className="flex items-center gap-3 font-medium text-base tracking-tight">
|
||||
<props.icon className="size-5" />
|
||||
{props.title}
|
||||
</span>
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
import type { Metadata } from "next";
|
||||
import { PanelTopIcon } from "lucide-react";
|
||||
import { PageLayout } from "@/components/blocks/APIHeader";
|
||||
import { CodeExample } from "@/components/code/code-example";
|
||||
import { StyledConnectEmbed } from "@/components/styled-connect-embed";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import { PageLayout } from "../../../../components/blocks/APIHeader";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description:
|
||||
"Let users sign up with their email, phone number, social media accounts or directly with a wallet. Seamlessly integrate account abstraction and SIWE auth.",
|
||||
metadataBase,
|
||||
title: "Sign In, Account Abstraction and SIWE Auth | thirdweb ConnectEmbed",
|
||||
};
|
||||
const title = "Connect Embed";
|
||||
const description =
|
||||
"Embeddable wallet component to manage logged in wallet states, view wallet balance, view assets, or buy and receive funds within an application.";
|
||||
const ogDescription =
|
||||
"Wallet component to manage logged in wallet, view wallet balance, view assets, or buy and receive funds within an application";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description: ogDescription,
|
||||
title,
|
||||
image: {
|
||||
icon: "wallets",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>
|
||||
Create a login experience tailor-made for your app. Add your wallets
|
||||
of choice, enable web2 sign-in options and create a modal that fits
|
||||
your brand.
|
||||
</>
|
||||
}
|
||||
icon={PanelTopIcon}
|
||||
title={title}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/wallets?utm_source=playground"
|
||||
title="ConnectEmbed"
|
||||
>
|
||||
<EmbedComponent />
|
||||
</PageLayout>
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
import type { Metadata } from "next";
|
||||
import { SquircleDashedIcon } from "lucide-react";
|
||||
import { PageLayout } from "@/components/blocks/APIHeader";
|
||||
import { CodeExample } from "@/components/code/code-example";
|
||||
import { HooksPreview } from "@/components/sign-in/hooks";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
import { ModalPreview } from "../../../../components/sign-in/modal";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description:
|
||||
"Let users sign up with their email, phone number, social media accounts or directly with a wallet. Seamlessly integrate account abstraction and SIWE auth.",
|
||||
metadataBase,
|
||||
title: "Sign In, Account Abstraction and SIWE Auth | thirdweb Connect",
|
||||
};
|
||||
const title = "Headless Connect";
|
||||
const description =
|
||||
"Create sign-in flow with your choice of wallets and sign-in options built custom to your branding. Use React hooks for full UI control and built-in wallet state management.";
|
||||
const ogDescription =
|
||||
"Build a custom login flow with your choice of wallets and sign-in options. Use React hooks for full UI control and built-in wallet state management.";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description: ogDescription,
|
||||
title,
|
||||
image: {
|
||||
icon: "wallets",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>
|
||||
Create a login experience tailor-made for your app. Add your wallets
|
||||
of choice, enable web2 sign-in options and create a modal that fits
|
||||
your brand.
|
||||
</>
|
||||
}
|
||||
icon={SquircleDashedIcon}
|
||||
title={title}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/wallets?utm_source=playground"
|
||||
title="Headless"
|
||||
>
|
||||
<div className="flex flex-col gap-14">
|
||||
<BuildCustomUISection />
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
import type { Metadata } from "next";
|
||||
import { GlobeIcon } from "lucide-react";
|
||||
import { PageLayout } from "@/components/blocks/APIHeader";
|
||||
import { CodeExample } from "@/components/code/code-example";
|
||||
import { SocialProfiles } from "@/components/social/social-profiles";
|
||||
import ThirdwebProvider from "@/components/thirdweb-provider";
|
||||
import { metadataBase } from "@/lib/constants";
|
||||
import { PageLayout } from "../../../components/blocks/APIHeader";
|
||||
import { createMetadata } from "@/lib/metadata";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description:
|
||||
"Retrieve any user's onchain identity from popular protocols like ENS, Lens, Farcaster, and more.",
|
||||
metadataBase,
|
||||
title: "Social APIs | thirdweb Connect",
|
||||
};
|
||||
const title = "Social Profiles";
|
||||
const description =
|
||||
"Enhance wallet authentication and context about user profiles with social identity data from ENS, Lens, and Farcaster";
|
||||
const ogDescription =
|
||||
"Enhance wallet authentication with social identity data from ENS, Lens, and Farcaster. Gain context-rich profiles on user login.";
|
||||
|
||||
export const metadata = createMetadata({
|
||||
description: ogDescription,
|
||||
title,
|
||||
image: {
|
||||
icon: "wallets",
|
||||
title,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<PageLayout
|
||||
description={
|
||||
<>
|
||||
Gain context about your users and their profiles across other apps
|
||||
as soon as they sign into your app.
|
||||
</>
|
||||
}
|
||||
icon={GlobeIcon}
|
||||
title={title}
|
||||
description={description}
|
||||
docsLink="https://portal.thirdweb.com/wallets?utm_source=playground"
|
||||
title="Get any user's onchain identity" // TODO: update this once we have Social API docs
|
||||
>
|
||||
<UserProfiles />
|
||||
</PageLayout>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { ClientOnly } from "@/components/ClientOnly";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
return (
|
||||
<div className="border-t pt-2">
|
||||
<ClientOnly
|
||||
ssr={<Skeleton className="size-[34px] rounded-full border bg-accent" />}
|
||||
>
|
||||
<Button
|
||||
aria-label="Toggle theme"
|
||||
className="w-full text-muted-foreground hover:text-foreground px-2 py-1.5 text-left justify-start gap-2 capitalize h-auto font-normal"
|
||||
onClick={() => {
|
||||
setTheme(theme === "dark" ? "light" : "dark");
|
||||
}}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<SunIcon className="size-4" />
|
||||
) : (
|
||||
<MoonIcon className="size-4" />
|
||||
)}
|
||||
|
||||
{theme}
|
||||
</Button>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,26 +11,8 @@ import {
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
import { inAppWallet, socialIcons } from "thirdweb/wallets/in-app";
|
||||
import { THIRDWEB_CLIENT } from "../../lib/client";
|
||||
import { StyledConnectButton } from "../styled-connect-button";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
export function ConnectSmartAccountPreview() {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<StyledConnectButton
|
||||
accountAbstraction={{
|
||||
chain: baseSepolia,
|
||||
sponsorGas: true,
|
||||
}}
|
||||
detailsModal={{
|
||||
// We hide the in-app wallet so they can't switch to it
|
||||
hiddenWallets: ["inApp"],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ConnectSmartAccountCustomPreview() {
|
||||
const account = useActiveAccount();
|
||||
const wallet = useActiveWallet();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BookOpenIcon, PresentationIcon } from "lucide-react";
|
||||
import { ArrowUpRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Button } from "../ui/button";
|
||||
@@ -7,38 +7,40 @@ export function PageHeader(props: {
|
||||
title: string;
|
||||
description: React.ReactNode;
|
||||
docsLink: string;
|
||||
icon: React.FC<{ className?: string }>;
|
||||
containerClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-8 border-b py-10">
|
||||
<div className="container grid grid-cols-1 items-start justify-between gap-6 lg:grid-cols-[1fr_340px] lg:flex-row">
|
||||
<div className="mb-8 border-b py-8 relative">
|
||||
<div
|
||||
className={cn(
|
||||
"container max-w-7xl flex-col lg:flex-row flex items-start justify-between gap-4",
|
||||
props.containerClassName,
|
||||
)}
|
||||
>
|
||||
{/* Left */}
|
||||
<div>
|
||||
<h1 className="mb-1 font-bold text-3xl tracking-tight lg:text-4xl">
|
||||
<div className="flex mb-4">
|
||||
<div className="rounded-full border bg-card p-2.5">
|
||||
<props.icon className="size-6 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 className="mb-0.5 font-semibold text-3xl tracking-tight">
|
||||
{props.title}
|
||||
</h1>
|
||||
<p className="text-balance text-base text-muted-foreground">
|
||||
<p className="text-sm md:text-base text-muted-foreground max-w-4xl">
|
||||
{props.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* right */}
|
||||
<div className="flex flex-col gap-3 md:flex-row">
|
||||
<Button asChild>
|
||||
<Link
|
||||
href={"https://thirdweb.com/dashboard?utm_source=playground"}
|
||||
target="_blank"
|
||||
>
|
||||
<PresentationIcon className="mr-2 h-4 w-4" />
|
||||
Add instantly
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild className="bg-card" variant="outline">
|
||||
<Link href={props.docsLink} target="_blank">
|
||||
<BookOpenIcon className="mr-2 h-4 w-4" />
|
||||
Documentation
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<Button asChild className="rounded-full gap-2 px-4" size="sm">
|
||||
<Link href={props.docsLink} target="_blank">
|
||||
Documentation
|
||||
<ArrowUpRightIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -50,15 +52,17 @@ export function PageLayout(props: {
|
||||
docsLink: string;
|
||||
children: React.ReactNode;
|
||||
containerClassName?: string;
|
||||
icon: React.FC<{ className?: string }>;
|
||||
}) {
|
||||
return (
|
||||
<main>
|
||||
<PageHeader
|
||||
description={props.description}
|
||||
docsLink={props.docsLink}
|
||||
icon={props.icon}
|
||||
title={props.title}
|
||||
/>
|
||||
<div className={cn("container pb-8", props.containerClassName)}>
|
||||
<div className={cn("container max-w-7xl pb-8", props.containerClassName)}>
|
||||
{props.children}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { ChainIcon } from "@/components/blocks/ChainIcon";
|
||||
import { MultiSelect } from "@/components/blocks/multi-select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useAllChainsData } from "../../app/hooks/chains";
|
||||
import { SelectWithSearch } from "../ui/select-with-search";
|
||||
import { ChainIcon } from "./ChainIcon";
|
||||
import { MultiSelect } from "./multi-select";
|
||||
import { SelectWithSearch } from "@/components/ui/select-with-search";
|
||||
import { useAllChainsData } from "@/hooks/chains";
|
||||
|
||||
function cleanChainName(chainName: string) {
|
||||
return chainName.replace("Mainnet", "");
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Suspense, useMemo, useState } from "react";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import { NavLink } from "@/components/ui/NavLink";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubItem,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useFullPath } from "../../hooks/full-path";
|
||||
import { ThirdwebIcon } from "../../icons/ThirdwebMiniLogo";
|
||||
import { ThemeToggle } from "../ThemeToggle";
|
||||
|
||||
type ShadcnSidebarBaseLink = {
|
||||
href: string;
|
||||
label: React.ReactNode;
|
||||
exactMatch?: boolean;
|
||||
icon?: React.FC<{ className?: string }>;
|
||||
};
|
||||
|
||||
export type ShadcnSidebarLink =
|
||||
| ShadcnSidebarBaseLink
|
||||
| {
|
||||
group: string;
|
||||
links: ShadcnSidebarLink[];
|
||||
}
|
||||
| {
|
||||
separator: true;
|
||||
className?: string;
|
||||
}
|
||||
| {
|
||||
subMenu: {
|
||||
icon?: React.FC<{ className?: string }>;
|
||||
label: string;
|
||||
};
|
||||
links: ShadcnSidebarLink[];
|
||||
};
|
||||
|
||||
export function FullWidthSidebarLayout(props: {
|
||||
contentSidebarLinks: ShadcnSidebarLink[];
|
||||
footerSidebarLinks?: ShadcnSidebarLink[];
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const { contentSidebarLinks, children, footerSidebarLinks } = props;
|
||||
const sidebar = useSidebar();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex w-full flex-1 overflow-y-hidden",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{/* left - sidebar */}
|
||||
<Sidebar collapsible="icon" side="left">
|
||||
<div
|
||||
className={cn("p-5", !sidebar.open && "p-0 py-5 flex justify-center")}
|
||||
>
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<ThirdwebIcon
|
||||
className="size-5 text-muted-foreground"
|
||||
isMonoChrome
|
||||
/>
|
||||
{sidebar.open && (
|
||||
<span
|
||||
className={cn(
|
||||
"font-semibold text-lg tracking-tight leading-none",
|
||||
)}
|
||||
>
|
||||
Playground
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<SidebarSeparator />
|
||||
|
||||
<SidebarContent className="p-2 no-scrollbar">
|
||||
<RenderSidebarMenu links={contentSidebarLinks} />
|
||||
</SidebarContent>
|
||||
|
||||
{footerSidebarLinks && (
|
||||
<SidebarFooter className="pb-3 pt-0">
|
||||
<RenderSidebarMenu links={footerSidebarLinks} />
|
||||
<ThemeToggle />
|
||||
</SidebarFooter>
|
||||
)}
|
||||
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
|
||||
{/* right - content */}
|
||||
<div className="flex h-full flex-grow flex-col overflow-y-auto">
|
||||
<div className="border-b p-4 lg:hidden">
|
||||
<div className="flex items-center gap-2">
|
||||
<ThirdwebIcon
|
||||
className="size-6 text-muted-foreground"
|
||||
isMonoChrome
|
||||
/>
|
||||
<span className="font-semibold text-lg leading-none tracking-tight">
|
||||
Playground
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MobileSidebarTrigger
|
||||
links={[...contentSidebarLinks, ...(footerSidebarLinks || [])]}
|
||||
/>
|
||||
|
||||
<main className="flex min-w-0 grow flex-col max-sm:w-full">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileSidebarTrigger(props: { links: ShadcnSidebarLink[] }) {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<MobileSidebarTriggerInner links={props.links} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileSidebarTriggerInner(props: { links: ShadcnSidebarLink[] }) {
|
||||
const activeLink = useActiveShadcnSidebarLink(props.links);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 border-b px-4 py-4 lg:hidden">
|
||||
<SidebarTrigger className="size-4" />
|
||||
<Separator
|
||||
className="h-4 bg-muted-foreground/50"
|
||||
orientation="vertical"
|
||||
/>
|
||||
{activeLink && "subMenu" in activeLink && (
|
||||
<>
|
||||
<span className="text-sm">{activeLink.subMenu.label}</span>
|
||||
<ChevronRightIcon className="size-4 text-muted-foreground/50 -mx-1.5" />
|
||||
</>
|
||||
)}
|
||||
{activeLink && "label" in activeLink && (
|
||||
<span className="text-sm">{activeLink.label}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useActiveShadcnSidebarLink(links: ShadcnSidebarLink[]) {
|
||||
const pathname = useFullPath();
|
||||
|
||||
const activeLink = useMemo(() => {
|
||||
const isActive = (link: ShadcnSidebarLink): boolean => {
|
||||
if ("href" in link) {
|
||||
// Handle exact match
|
||||
if (link.exactMatch) {
|
||||
return link.href === pathname;
|
||||
}
|
||||
|
||||
// Handle prefix match (ensure we don't match partial paths)
|
||||
return pathname.startsWith(link.href);
|
||||
}
|
||||
|
||||
if ("links" in link) {
|
||||
return link.links.some(isActive);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
return links.find(isActive);
|
||||
}, [links, pathname]);
|
||||
|
||||
return activeLink;
|
||||
}
|
||||
|
||||
function useIsSubnavActive(links: ShadcnSidebarLink[]) {
|
||||
const pathname = useFullPath();
|
||||
|
||||
const isSubnavActive = useMemo(() => {
|
||||
const isActive = (link: ShadcnSidebarLink): boolean => {
|
||||
if ("href" in link) {
|
||||
// Handle exact match
|
||||
if (link.exactMatch) {
|
||||
return link.href === pathname;
|
||||
}
|
||||
|
||||
return pathname.startsWith(link.href);
|
||||
}
|
||||
|
||||
if ("links" in link) {
|
||||
return link.links.some(isActive);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
return links.some(isActive);
|
||||
}, [links, pathname]);
|
||||
|
||||
return isSubnavActive;
|
||||
}
|
||||
|
||||
function RenderSidebarGroup(props: {
|
||||
sidebarLinks: ShadcnSidebarLink[];
|
||||
groupName: string;
|
||||
}) {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<RenderSidebarGroupInner {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderSidebarGroupInner(props: {
|
||||
sidebarLinks: ShadcnSidebarLink[];
|
||||
groupName: string;
|
||||
}) {
|
||||
return (
|
||||
<SidebarGroup className="p-0">
|
||||
<SidebarMenuItem>
|
||||
<SidebarGroupLabel> {props.groupName}</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<RenderSidebarMenu links={props.sidebarLinks} />
|
||||
</SidebarGroupContent>
|
||||
</SidebarMenuItem>
|
||||
</SidebarGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderSidebarSubmenu(props: {
|
||||
links: ShadcnSidebarLink[];
|
||||
subMenu: Omit<ShadcnSidebarBaseLink, "href" | "exactMatch">;
|
||||
}) {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<RenderSidebarSubmenuInner {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderSidebarSubmenuInner(props: {
|
||||
links: ShadcnSidebarLink[];
|
||||
subMenu: Omit<ShadcnSidebarBaseLink, "href" | "exactMatch">;
|
||||
}) {
|
||||
const sidebar = useSidebar();
|
||||
const isSubnavActive = useIsSubnavActive(props.links);
|
||||
const [_open, setOpen] = useState<boolean | undefined>(undefined);
|
||||
const open = _open === undefined ? isSubnavActive : _open;
|
||||
|
||||
// Add null check for links
|
||||
if (!props.links || props.links.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarMenu>
|
||||
<DynamicHeight transition="height 200ms ease">
|
||||
<SidebarMenuItem>
|
||||
<Collapsible
|
||||
className="group/collapsible [&[data-state=open]>button>svg[data-chevron]]:rotate-180"
|
||||
defaultOpen={open}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<SidebarMenuButton
|
||||
className={cn(
|
||||
"text-sm text-muted-foreground whitespace-nowrap hover:bg-accent hover:text-foreground",
|
||||
isSubnavActive && "text-foreground",
|
||||
)}
|
||||
>
|
||||
{props.subMenu.icon && (
|
||||
<props.subMenu.icon className="size-4" />
|
||||
)}
|
||||
<span>{props.subMenu.label}</span>
|
||||
|
||||
{sidebar.open && (
|
||||
<ChevronDownIcon
|
||||
data-chevron
|
||||
className="transition-transform ml-auto"
|
||||
/>
|
||||
)}
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<CollapsibleContent className="mt-0.5 [&>ul]:mr-0">
|
||||
<SidebarMenuSub className="pr-0 pl-2">
|
||||
{props.links.map((link, index) => {
|
||||
if ("href" in link) {
|
||||
return (
|
||||
<SidebarMenuSubItem key={link.href}>
|
||||
<NavLink
|
||||
key={link.href}
|
||||
activeClassName="text-foreground bg-accent "
|
||||
className="flex items-center gap-2 text-muted-foreground text-sm hover:bg-accent hover:text-foreground px-2 py-1.5 rounded-lg w-full"
|
||||
exactMatch={link.exactMatch}
|
||||
href={link.href}
|
||||
onClick={() => {
|
||||
sidebar.setOpenMobile(false);
|
||||
}}
|
||||
>
|
||||
{link.icon && <link.icon className="size-4" />}
|
||||
<span>{link.label}</span>
|
||||
</NavLink>
|
||||
</SidebarMenuSubItem>
|
||||
);
|
||||
}
|
||||
|
||||
if ("subMenu" in link) {
|
||||
return (
|
||||
<RenderSidebarSubmenu
|
||||
key={`submenu_${link.subMenu.label}_${index}`}
|
||||
links={link.links}
|
||||
subMenu={link.subMenu}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if ("group" in link) {
|
||||
return (
|
||||
<RenderSidebarGroup
|
||||
key={`group_${link.group}_${index}`}
|
||||
groupName={link.group}
|
||||
sidebarLinks={link.links}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</SidebarMenuSub>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</SidebarMenuItem>
|
||||
</DynamicHeight>
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderSidebarMenu(props: { links: ShadcnSidebarLink[] }) {
|
||||
const sidebar = useSidebar();
|
||||
|
||||
// Add null check for links
|
||||
if (!props.links || props.links.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarMenu className="gap-1.5">
|
||||
{props.links.map((link, idx) => {
|
||||
// link
|
||||
if ("href" in link) {
|
||||
return (
|
||||
<SidebarMenuSubItem key={link.href}>
|
||||
<SidebarMenuButton asChild>
|
||||
<NavLink
|
||||
activeClassName="text-foreground bg-accent"
|
||||
className="flex items-center gap-2 text-muted-foreground text-sm hover:bg-accent"
|
||||
exactMatch={link.exactMatch}
|
||||
href={link.href}
|
||||
onClick={() => {
|
||||
sidebar.setOpenMobile(false);
|
||||
}}
|
||||
>
|
||||
{link.icon && <link.icon className="size-4" />}
|
||||
<span>{link.label}</span>
|
||||
</NavLink>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuSubItem>
|
||||
);
|
||||
}
|
||||
|
||||
// separator
|
||||
if ("separator" in link) {
|
||||
return (
|
||||
<SidebarSeparator
|
||||
className={cn("my-1", link.className)}
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey:ok
|
||||
key={idx}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// subnav
|
||||
if ("subMenu" in link) {
|
||||
return (
|
||||
<RenderSidebarSubmenu
|
||||
key={`submenu_${link.subMenu.label}_${idx}`}
|
||||
links={link.links}
|
||||
subMenu={link.subMenu}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// group
|
||||
if ("group" in link) {
|
||||
return (
|
||||
<RenderSidebarGroup
|
||||
groupName={link.group}
|
||||
sidebarLinks={link.links}
|
||||
key={`group_${link.group}_${idx}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
@@ -145,7 +145,7 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
|
||||
// scroll to top when options change
|
||||
const popoverElRef = useRef<HTMLDivElement>(null);
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: ok
|
||||
useEffect(() => {
|
||||
const scrollContainer =
|
||||
popoverElRef.current?.querySelector("[data-scrollable]");
|
||||
|
||||
@@ -22,7 +22,7 @@ export function RenderCode(props: {
|
||||
props.scrollableContainerClassName,
|
||||
)}
|
||||
scrollableClassName={cn("p-4", props.scrollableClassName)}
|
||||
shadowColor="hsl(var(--muted))"
|
||||
shadowColor="hsl(var(--card))"
|
||||
>
|
||||
<div
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: we know what we're doing here
|
||||
|
||||
@@ -23,11 +23,11 @@ export const CodeExample: React.FC<CodeExampleProps> = ({
|
||||
return (
|
||||
<div className="relative z-0">
|
||||
{header && (
|
||||
<div className="mb-4 space-y-0.5">
|
||||
<h2 className="font-semibold text-2xl tracking-tight">
|
||||
<div className="mb-4">
|
||||
<h2 className="font-semibold text-xl tracking-tight capitalize">
|
||||
{header.title}
|
||||
</h2>
|
||||
<p className="max-w-3xl text-muted-foreground ">
|
||||
<p className="max-w-4xl text-muted-foreground text-balance text-sm md:text-base">
|
||||
{header.description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -44,7 +44,7 @@ export const CodeExample: React.FC<CodeExampleProps> = ({
|
||||
<div className="flex grow flex-col">
|
||||
<TabName icon={EyeIcon} name="Preview" />
|
||||
<ClientOnly
|
||||
className="relative grid h-full min-h-[300px] place-items-center bg-background py-20"
|
||||
className="relative grid h-full min-h-[300px] place-items-center bg-card py-20"
|
||||
ssr={null}
|
||||
>
|
||||
{preview}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { WalletIcon, WalletName, WalletProvider } from "thirdweb/react";
|
||||
|
||||
export function WalletIconBasicPreview() {
|
||||
return (
|
||||
// biome-ignore lint/nursery/useUniqueElementIds:ID is not the html attribute in this case
|
||||
<WalletProvider id="io.metamask">
|
||||
<WalletIcon
|
||||
className="h-20 w-20 rounded-full"
|
||||
@@ -16,7 +15,6 @@ export function WalletIconBasicPreview() {
|
||||
|
||||
export function WalletNameBasicPreview() {
|
||||
return (
|
||||
// biome-ignore lint/nursery/useUniqueElementIds:ID is not the html attribute in this case
|
||||
<WalletProvider id="io.metamask">
|
||||
<WalletName loadingComponent={<span>Loading...</span>} />
|
||||
</WalletProvider>
|
||||
@@ -25,7 +23,6 @@ export function WalletNameBasicPreview() {
|
||||
|
||||
export function WalletNameFormatPreview() {
|
||||
return (
|
||||
// biome-ignore lint/nursery/useUniqueElementIds:ID is not the html attribute in this case
|
||||
<WalletProvider id="io.metamask">
|
||||
<WalletName
|
||||
formatFn={(str: string) => `${str} Wallet`}
|
||||
|
||||
@@ -5,19 +5,17 @@ import { THIRDWEB_CLIENT } from "../../lib/client";
|
||||
|
||||
export function BuyMerchPreview() {
|
||||
return (
|
||||
<>
|
||||
<CheckoutWidget
|
||||
amount={"2"}
|
||||
chain={base}
|
||||
client={THIRDWEB_CLIENT}
|
||||
description="Get your ticket for The Midnight Live"
|
||||
feePayer="seller"
|
||||
image="https://images.unsplash.com/photo-1501281668745-f7f57925c3b4?w=500&h=300&fit=crop"
|
||||
name="Concert Ticket"
|
||||
seller="0xEb0effdFB4dC5b3d5d3aC6ce29F3ED213E95d675"
|
||||
theme="light"
|
||||
tokenAddress="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
||||
/>
|
||||
</>
|
||||
<CheckoutWidget
|
||||
amount={"2"}
|
||||
chain={base}
|
||||
client={THIRDWEB_CLIENT}
|
||||
description="Get your ticket for The Midnight Live"
|
||||
feePayer="seller"
|
||||
image="https://images.unsplash.com/photo-1501281668745-f7f57925c3b4?w=500&h=300&fit=crop"
|
||||
name="Concert Ticket"
|
||||
seller="0xEb0effdFB4dC5b3d5d3aC6ce29F3ED213E95d675"
|
||||
theme="light"
|
||||
tokenAddress="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Suspense } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useFullPath } from "../../hooks/full-path";
|
||||
|
||||
export type NavButtonProps = {
|
||||
className?: string;
|
||||
activeClassName?: string;
|
||||
href: string;
|
||||
exactMatch?: boolean;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export function NavLink(props: React.PropsWithChildren<NavButtonProps>) {
|
||||
return (
|
||||
<Suspense>
|
||||
<NavLinkInner {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function NavLinkInner(props: React.PropsWithChildren<NavButtonProps>) {
|
||||
const pathname = useFullPath();
|
||||
|
||||
const isActive = props.exactMatch
|
||||
? pathname === props.href
|
||||
: pathname.startsWith(props.href);
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={cn(props.className, isActive && props.activeClassName)}
|
||||
href={props.href}
|
||||
onClick={props.onClick}
|
||||
prefetch={false}
|
||||
target={props.href.startsWith("http") ? "_blank" : undefined}
|
||||
>
|
||||
{props.children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -17,10 +17,10 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useAllChainsData } from "@/hooks/chains";
|
||||
import { useTokensData } from "@/hooks/useTokensData";
|
||||
import type { TokenMetadata } from "@/lib/types";
|
||||
import { cn, fallbackChainIcon, replaceIpfsUrl } from "@/lib/utils";
|
||||
import { useAllChainsData } from "../../app/hooks/chains";
|
||||
|
||||
const checksummedNativeTokenAddress = getAddress(NATIVE_TOKEN_ADDRESS);
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
||||
|
||||
const Collapsible = CollapsiblePrimitive.Root;
|
||||
|
||||
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
||||
|
||||
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
||||
@@ -20,7 +20,7 @@ const SelectTrigger = React.forwardRef<
|
||||
>(({ className, children, chevronClassName, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 [&>span]:w-full",
|
||||
"flex h-10 w-full items-center justify-between gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
@@ -119,7 +119,7 @@ const SelectItem = React.forwardRef<
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>span]:w-full",
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
@@ -127,7 +127,7 @@ const SelectItem = React.forwardRef<
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
"use client";
|
||||
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { XIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger;
|
||||
|
||||
const SheetClose = SheetPrimitive.Close;
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal;
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 border-border",
|
||||
{
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
variants: {
|
||||
side: {
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="absolute top-4 right-4 rounded-sm border-border opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
));
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 border-border text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetHeader.displayName = "SheetHeader";
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse border-border sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetFooter.displayName = "SheetFooter";
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
className={cn(
|
||||
"border-border font-semibold text-foreground text-lg",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
className={cn("border-border text-muted-foreground text-sm", className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
};
|
||||
@@ -1,229 +1,774 @@
|
||||
/** biome-ignore-all lint/suspicious/noDocumentCookie: FIXME */
|
||||
"use client";
|
||||
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { Suspense, useRef } from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { PanelLeftIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ClientOnly } from "../ClientOnly";
|
||||
import { CustomAccordion } from "./CustomAccordion";
|
||||
|
||||
export type LinkMeta = {
|
||||
name: string;
|
||||
href: string;
|
||||
crossedOut?: boolean;
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
const SIDEBAR_WIDTH = "19rem";
|
||||
const SIDEBAR_WIDTH_MOBILE = "19rem";
|
||||
const SIDEBAR_WIDTH_ICON = "3rem";
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||
|
||||
type SidebarContextProps = {
|
||||
state: "expanded" | "collapsed";
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
openMobile: boolean;
|
||||
setOpenMobile: (open: boolean) => void;
|
||||
isMobile: boolean;
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
export type LinkGroup = {
|
||||
name: string;
|
||||
href?: string;
|
||||
links: SidebarLink[];
|
||||
expanded?: boolean;
|
||||
/**
|
||||
* If set to false, the group will not be rendered as an accordion
|
||||
* @defaultValue true
|
||||
*/
|
||||
isCollapsible?: boolean;
|
||||
};
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
|
||||
|
||||
export type SidebarLink = LinkMeta | LinkGroup | { separator: true };
|
||||
|
||||
type ReferenceSideBarProps = {
|
||||
links: SidebarLink[];
|
||||
onLinkClick?: () => void;
|
||||
};
|
||||
|
||||
export function Sidebar(props: ReferenceSideBarProps) {
|
||||
return (
|
||||
<ul className="transform-gpu">
|
||||
{props.links.map((link, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
|
||||
<li key={i}>
|
||||
<Suspense fallback={null}>
|
||||
<SidebarItem link={link} onLinkClick={props.onLinkClick} />
|
||||
</Suspense>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarItem(props: { link: SidebarLink; onLinkClick?: () => void }) {
|
||||
const href = useCurrentHref();
|
||||
|
||||
if ("separator" in props.link) {
|
||||
return <hr className="my-2 border-t" />;
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext);
|
||||
if (!context) {
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.");
|
||||
}
|
||||
|
||||
const isActive = props.link.href ? isSamePage(href, props.link.href) : false;
|
||||
return context;
|
||||
}
|
||||
|
||||
const { link } = props;
|
||||
if ("links" in link) {
|
||||
if (link.isCollapsible === false) {
|
||||
const SidebarProvider = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
defaultOpen?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
defaultOpen = true,
|
||||
open: openProp,
|
||||
onOpenChange: setOpenProp,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const isMobile = useIsMobile();
|
||||
const [openMobile, setOpenMobile] = React.useState(false);
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen);
|
||||
const open = openProp ?? _open;
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === "function" ? value(open) : value;
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState);
|
||||
} else {
|
||||
_setOpen(openState);
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||
},
|
||||
[setOpenProp, open],
|
||||
);
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile
|
||||
? setOpenMobile((open) => !open)
|
||||
: setOpen((open) => !open);
|
||||
}, [isMobile, setOpen]);
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [toggleSidebar]);
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? "expanded" : "collapsed";
|
||||
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
() => ({
|
||||
isMobile,
|
||||
open,
|
||||
openMobile,
|
||||
setOpen,
|
||||
setOpenMobile,
|
||||
state,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, toggleSidebar],
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div
|
||||
className={cn(
|
||||
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH,
|
||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
SidebarProvider.displayName = "SidebarProvider";
|
||||
|
||||
const Sidebar = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
side?: "left" | "right";
|
||||
variant?: "sidebar" | "floating" | "inset";
|
||||
collapsible?: "offcanvas" | "icon" | "none";
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
side = "left",
|
||||
variant = "sidebar",
|
||||
collapsible = "offcanvas",
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
<DocSidebarNonCollapsible
|
||||
key={link.name}
|
||||
linkGroup={link}
|
||||
onLinkClick={props.onLinkClick}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SidebarCategory
|
||||
key={link.name}
|
||||
linkGroup={link}
|
||||
onLinkClick={props.onLinkClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={clsx(
|
||||
"block overflow-hidden text-ellipsis py-1 font-medium transition-colors duration-300 hover:text-foreground lg:text-base",
|
||||
isActive ? "text-foreground" : "text-muted-foreground",
|
||||
"crossedOut" in link && link.crossedOut && "line-through",
|
||||
)}
|
||||
href={link.href}
|
||||
onClick={props.onLinkClick}
|
||||
>
|
||||
{link.name}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function DocSidebarNonCollapsible(props: {
|
||||
linkGroup: LinkGroup;
|
||||
onLinkClick?: () => void;
|
||||
}) {
|
||||
const currentHref = useCurrentHref();
|
||||
const { href, name, links } = props.linkGroup;
|
||||
const isCategoryActive = href ? isSamePage(currentHref, href) : false;
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
{href ? (
|
||||
<Link
|
||||
className={cn(
|
||||
"block font-medium text-muted-foreground hover:text-foreground lg:text-base",
|
||||
isCategoryActive && "!text-foreground",
|
||||
)}
|
||||
href={href}
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet onOpenChange={setOpenMobile} open={openMobile} {...props}>
|
||||
<SheetContent
|
||||
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
||||
data-mobile="true"
|
||||
data-sidebar="sidebar"
|
||||
side={side}
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
) : (
|
||||
<div className="font-medium text-lg">{name}</div>
|
||||
)}
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group peer relative hidden flex-shrink-0 text-sidebar-foreground md:block"
|
||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||
data-side={side}
|
||||
data-state={state}
|
||||
data-variant={variant}
|
||||
ref={ref}
|
||||
>
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
|
||||
"group-data-[collapsible=offcanvas]:w-0",
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]",
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-y-0 z-10 hidden h-full w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
|
||||
data-sidebar="sidebar"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul className="mb-8 flex flex-col">
|
||||
{links.map((link, i) => {
|
||||
return (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: order is static
|
||||
<li key={i}>
|
||||
<SidebarItem link={link} onLinkClick={props.onLinkClick} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
Sidebar.displayName = "Sidebar";
|
||||
|
||||
const SidebarTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof Button>,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, onClick, ...props }, ref) => {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={cn("h-7 w-7", className)}
|
||||
data-sidebar="trigger"
|
||||
onClick={(event) => {
|
||||
onClick?.(event);
|
||||
toggleSidebar();
|
||||
}}
|
||||
ref={ref}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
{...props}
|
||||
>
|
||||
<PanelLeftIcon />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
});
|
||||
SidebarTrigger.displayName = "SidebarTrigger";
|
||||
|
||||
function useCurrentHref() {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
return decodeURIComponent(
|
||||
`${pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ""}`,
|
||||
const SidebarRail = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button">
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<button
|
||||
aria-label="Toggle Sidebar"
|
||||
className={cn(
|
||||
"-translate-x-1/2 group-data-[side=left]:-right-4 absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=right]:left-0 sm:flex",
|
||||
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:hover:bg-sidebar group-data-[collapsible=offcanvas]:after:left-full",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||
className,
|
||||
)}
|
||||
data-sidebar="rail"
|
||||
onClick={toggleSidebar}
|
||||
ref={ref}
|
||||
tabIndex={-1}
|
||||
title="Toggle Sidebar"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
SidebarRail.displayName = "SidebarRail";
|
||||
|
||||
function SidebarCategory(props: {
|
||||
linkGroup: LinkGroup;
|
||||
onLinkClick?: () => void;
|
||||
}) {
|
||||
const currentPageHref = useCurrentHref();
|
||||
const { href, name, links, expanded } = props.linkGroup;
|
||||
const isCategoryActive = href ? isSamePage(currentPageHref, href) : false;
|
||||
|
||||
const hasActiveHref = containsActiveHref(
|
||||
{
|
||||
href: href,
|
||||
links: links,
|
||||
name: name,
|
||||
},
|
||||
currentPageHref,
|
||||
const SidebarInset = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"main">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<main
|
||||
className={cn(
|
||||
"relative flex w-full flex-1 flex-col bg-background",
|
||||
"md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarInset.displayName = "SidebarInset";
|
||||
|
||||
const defaultOpen = isCategoryActive || !!(hasActiveHref || expanded);
|
||||
const SidebarInput = React.forwardRef<
|
||||
React.ElementRef<typeof Input>,
|
||||
React.ComponentProps<typeof Input>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Input
|
||||
className={cn(
|
||||
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
|
||||
className,
|
||||
)}
|
||||
data-sidebar="input"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarInput.displayName = "SidebarInput";
|
||||
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
const SidebarHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
data-sidebar="header"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarHeader.displayName = "SidebarHeader";
|
||||
|
||||
const triggerElContent = (
|
||||
const SidebarFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
data-sidebar="footer"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarFooter.displayName = "SidebarFooter";
|
||||
|
||||
const SidebarSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof Separator>,
|
||||
React.ComponentProps<typeof Separator>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Separator
|
||||
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
||||
data-sidebar="separator"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarSeparator.displayName = "SidebarSeparator";
|
||||
|
||||
const SidebarContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
isCategoryActive && "!font-semibold !text-foreground",
|
||||
"group-hover:!text-foreground text-muted-foreground",
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-2 py-1 font-medium" ref={triggerRef}>
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
data-sidebar="content"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarContent.displayName = "SidebarContent";
|
||||
|
||||
const triggerEl = href ? (
|
||||
<Link className={cn("block w-full text-left font-medium")} href={href}>
|
||||
{triggerElContent}
|
||||
</Link>
|
||||
) : (
|
||||
triggerElContent
|
||||
const SidebarGroup = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||
data-sidebar="group"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarGroup.displayName = "SidebarGroup";
|
||||
|
||||
const SidebarGroupLabel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & { asChild?: boolean }
|
||||
>(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "div";
|
||||
|
||||
return (
|
||||
<ClientOnly ssr={triggerEl}>
|
||||
<CustomAccordion
|
||||
chevronPosition="right"
|
||||
containerClassName="border-none"
|
||||
defaultOpen={defaultOpen}
|
||||
trigger={triggerEl}
|
||||
triggerContainerClassName="lg:text-base group"
|
||||
>
|
||||
<ul className="flex flex-col border-l pl-4">
|
||||
{links.map((link, i) => {
|
||||
return (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: order is static
|
||||
<li key={i}>
|
||||
<SidebarItem link={link} onLinkClick={props.onLinkClick} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</CustomAccordion>
|
||||
</ClientOnly>
|
||||
<Comp
|
||||
className={cn(
|
||||
"flex h-8 shrink-0 items-center rounded-md px-2 font-medium text-sidebar-foreground/70 text-xs outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className,
|
||||
)}
|
||||
data-sidebar="group-label"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
});
|
||||
SidebarGroupLabel.displayName = "SidebarGroupLabel";
|
||||
|
||||
function containsActiveHref(
|
||||
sidebarlink: SidebarLink,
|
||||
pathname: string,
|
||||
): boolean {
|
||||
if ("links" in sidebarlink) {
|
||||
return sidebarlink.links.some((link) => containsActiveHref(link, pathname));
|
||||
const SidebarGroupAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & { asChild?: boolean }
|
||||
>(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
className={cn(
|
||||
"absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:-inset-2 after:absolute after:md:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
data-sidebar="group-action"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarGroupAction.displayName = "SidebarGroupAction";
|
||||
|
||||
const SidebarGroupContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
className={cn("w-full text-sm", className)}
|
||||
data-sidebar="group-content"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarGroupContent.displayName = "SidebarGroupContent";
|
||||
|
||||
const SidebarMenu = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<"ul">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||
data-sidebar="menu"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarMenu.displayName = "SidebarMenu";
|
||||
|
||||
const SidebarMenuItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<"li">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li
|
||||
className={cn("group/menu-item relative", className)}
|
||||
data-sidebar="menu-item"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarMenuItem.displayName = "SidebarMenuItem";
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
{
|
||||
defaultVariants: {
|
||||
size: "default",
|
||||
variant: "default",
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
|
||||
sm: "h-7 text-xs",
|
||||
},
|
||||
variant: {
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
outline:
|
||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const SidebarMenuButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & {
|
||||
asChild?: boolean;
|
||||
isActive?: boolean;
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>
|
||||
>(
|
||||
(
|
||||
{
|
||||
asChild = false,
|
||||
isActive = false,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const { isMobile, state } = useSidebar();
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
className={cn(sidebarMenuButtonVariants({ size, variant }), className)}
|
||||
data-active={isActive}
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!tooltip) {
|
||||
return button;
|
||||
}
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
align="center"
|
||||
hidden={state !== "collapsed" || isMobile}
|
||||
side="right"
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
);
|
||||
SidebarMenuButton.displayName = "SidebarMenuButton";
|
||||
|
||||
const SidebarMenuAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & {
|
||||
asChild?: boolean;
|
||||
showOnHover?: boolean;
|
||||
}
|
||||
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
if ("separator" in sidebarlink) {
|
||||
return false;
|
||||
return (
|
||||
<Comp
|
||||
className={cn(
|
||||
"absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:-inset-2 after:absolute after:md:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
showOnHover &&
|
||||
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
|
||||
className,
|
||||
)}
|
||||
data-sidebar="menu-action"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarMenuAction.displayName = "SidebarMenuAction";
|
||||
|
||||
const SidebarMenuBadge = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 font-medium text-sidebar-foreground text-xs tabular-nums",
|
||||
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
data-sidebar="menu-badge"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarMenuBadge.displayName = "SidebarMenuBadge";
|
||||
|
||||
const SidebarMenuSkeleton = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
showIcon?: boolean;
|
||||
}
|
||||
>(({ className, showIcon = false, ...props }, ref) => {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
}, []);
|
||||
|
||||
if (isSamePage(pathname, sidebarlink.href)) {
|
||||
return true;
|
||||
return (
|
||||
<div
|
||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||
data-sidebar="menu-skeleton"
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && (
|
||||
<Skeleton
|
||||
className="size-4 rounded-md"
|
||||
data-sidebar="menu-skeleton-icon"
|
||||
/>
|
||||
)}
|
||||
<Skeleton
|
||||
className="h-4 max-w-[--skeleton-width] flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
"--skeleton-width": width,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
|
||||
|
||||
const SidebarMenuSub = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<"ul">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
className={cn(
|
||||
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-sidebar-border border-l px-2.5 py-0.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
data-sidebar="menu-sub"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarMenuSub.displayName = "SidebarMenuSub";
|
||||
|
||||
const SidebarMenuSubItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<"li">
|
||||
>(({ ...props }, ref) => <li ref={ref} {...props} />);
|
||||
SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
|
||||
|
||||
const SidebarMenuSubButton = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentProps<"a"> & {
|
||||
asChild?: boolean;
|
||||
size?: "sm" | "md";
|
||||
isActive?: boolean;
|
||||
}
|
||||
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
<Comp
|
||||
className={cn(
|
||||
"-translate-x-px flex h-7 min-w-0 items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
data-active={isActive}
|
||||
data-sidebar="menu-sub-button"
|
||||
data-size={size}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
|
||||
|
||||
function isSamePage(currentPageHref: string, testHref: string): boolean {
|
||||
return currentPageHref === testHref;
|
||||
}
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupAction,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuBadge,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-skeleton rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SkeletonContainer<T>(props: {
|
||||
loadedData?: T;
|
||||
skeletonData: T;
|
||||
className?: string;
|
||||
render: (data: T) => React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
}) {
|
||||
const isLoading = props.loadedData === undefined;
|
||||
return (
|
||||
<div
|
||||
aria-hidden={isLoading ? "true" : "false"}
|
||||
className={cn(
|
||||
isLoading && "inline-block animate-pulse rounded-lg bg-muted",
|
||||
props.className,
|
||||
)}
|
||||
style={props.style}
|
||||
>
|
||||
<div className={cn(isLoading && "invisible")}>
|
||||
<div
|
||||
className={cn(
|
||||
"transition-opacity duration-300",
|
||||
isLoading ? "opacity-0" : "opacity-100",
|
||||
)}
|
||||
>
|
||||
{props.render(
|
||||
props.loadedData === undefined
|
||||
? props.skeletonData
|
||||
: props.loadedData,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton, SkeletonContainer };
|
||||
@@ -0,0 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
|
||||
export function useFullPath() {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const searchParamsString = searchParams.toString();
|
||||
return decodeURIComponent(
|
||||
`${pathname}${searchParamsString ? `?${searchParamsString}` : ""}`,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as React from "react";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener("change", onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export function ContractIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
fill="none"
|
||||
height="24"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14 11H8M10 15H8M16 7H8M20 6.8V17.2C20 18.8802 20 19.7202 19.673 20.362C19.3854 20.9265 18.9265 21.3854 18.362 21.673C17.7202 22 16.8802 22 15.2 22H8.8C7.11984 22 6.27976 22 5.63803 21.673C5.07354 21.3854 4.6146 20.9265 4.32698 20.362C4 19.7202 4 18.8802 4 17.2V6.8C4 5.11984 4 4.27976 4.32698 3.63803C4.6146 3.07354 5.07354 2.6146 5.63803 2.32698C6.27976 2 7.11984 2 8.8 2H15.2C16.8802 2 17.7202 2 18.362 2.32698C18.9265 2.6146 19.3854 3.07354 19.673 3.63803C20 4.27976 20 5.11984 20 6.8Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export function DashboardIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="presentation"
|
||||
className={props.className}
|
||||
>
|
||||
<path
|
||||
d="M3 9H21M9 9L9 21M7.8 3H16.2C17.8802 3 18.7202 3 19.362 3.32698C19.9265 3.6146 20.3854 4.07354 20.673 4.63803C21 5.27976 21 6.11984 21 7.8V16.2C21 17.8802 21 18.7202 20.673 19.362C20.3854 19.9265 19.9265 20.3854 19.362 20.673C18.7202 21 17.8802 21 16.2 21H7.8C6.11984 21 5.27976 21 4.63803 20.673C4.07354 20.3854 3.6146 19.9265 3.32698 19.362C3 18.7202 3 17.8802 3 16.2V7.8C3 6.11984 3 5.27976 3.32698 4.63803C3.6146 4.07354 4.07354 3.6146 4.63803 3.32698C5.27976 3 6.11984 3 7.8 3Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export function InsightIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
fill="none"
|
||||
height="24"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M20.5 7.27783L12 12.0001M12 12.0001L3.49997 7.27783M12 12.0001L12 21.5001M21 12.0001V7.94153C21 7.59889 21 7.42757 20.9495 7.27477C20.9049 7.13959 20.8318 7.01551 20.7354 6.91082C20.6263 6.79248 20.4766 6.70928 20.177 6.54288L12.777 2.43177C12.4934 2.27421 12.3516 2.19543 12.2015 2.16454C12.0685 2.13721 11.9315 2.13721 11.7986 2.16454C11.6484 2.19543 11.5066 2.27421 11.223 2.43177L3.82297 6.54288C3.52345 6.70928 3.37368 6.79248 3.26463 6.91082C3.16816 7.01551 3.09515 7.13959 3.05048 7.27477C3 7.42757 3 7.59889 3 7.94153V16.0586C3 16.4013 3 16.5726 3.05048 16.7254C3.09515 16.8606 3.16816 16.9847 3.26463 17.0893C3.37368 17.2077 3.52346 17.2909 3.82297 17.4573L11.223 21.5684C11.5066 21.726 11.6484 21.8047 11.7986 21.8356C11.9315 21.863 12.0685 21.863 12.2015 21.8356C12.3516 21.8047 12.4934 21.726 12.777 21.5684L13 21.4445M7.5 4.50008L16.5 9.50008M22 21.5001L21 20.5001M22 18.0001C22 19.6569 20.6569 21.0001 19 21.0001C17.3431 21.0001 16 19.6569 16 18.0001C16 16.3432 17.3431 15.0001 19 15.0001C20.6569 15.0001 22 16.3432 22 18.0001Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export function PayIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
fill="none"
|
||||
height="24"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M22 10H2M2 8.2L2 15.8C2 16.9201 2 17.4802 2.21799 17.908C2.40973 18.2843 2.71569 18.5903 3.09202 18.782C3.51984 19 4.07989 19 5.2 19L18.8 19C19.9201 19 20.4802 19 20.908 18.782C21.2843 18.5903 21.5903 18.2843 21.782 17.908C22 17.4802 22 16.9201 22 15.8V8.2C22 7.0799 22 6.51984 21.782 6.09202C21.5903 5.7157 21.2843 5.40974 20.908 5.21799C20.4802 5 19.9201 5 18.8 5L5.2 5C4.0799 5 3.51984 5 3.09202 5.21799C2.7157 5.40973 2.40973 5.71569 2.21799 6.09202C2 6.51984 2 7.07989 2 8.2Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export function SmartAccountIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={props.className}
|
||||
fill="none"
|
||||
height="24"
|
||||
role="presentation"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M13 7L11.8845 4.76892C11.5634 4.1268 11.4029 3.80573 11.1634 3.57116C10.9516 3.36373 10.6963 3.20597 10.4161 3.10931C10.0992 3 9.74021 3 9.02229 3H5.2C4.0799 3 3.51984 3 3.09202 3.21799C2.71569 3.40973 2.40973 3.71569 2.21799 4.09202C2 4.51984 2 5.0799 2 6.2V7M2 7H17.2C18.8802 7 19.7202 7 20.362 7.32698C20.9265 7.6146 21.3854 8.07354 21.673 8.63803C22 9.27976 22 10.1198 22 11.8V16.2C22 17.8802 22 18.7202 21.673 19.362C21.3854 19.9265 20.9265 20.3854 20.362 20.673C19.7202 21 18.8802 21 17.2 21H6.8C5.11984 21 4.27976 21 3.63803 20.673C3.07354 20.3854 2.6146 19.9265 2.32698 19.362C2 18.7202 2 17.8802 2 16.2V7ZM12 17.5C12 17.5 15 16.0701 15 13.9252V11.4229L12.8124 10.6412C12.2868 10.4529 11.712 10.4529 11.1864 10.6412L9 11.4229V13.9252C9 16.0701 12 17.5 12 17.5Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useId } from "react";
|
||||
|
||||
function prefixId(id: string, prefix?: string) {
|
||||
return prefix ? `${prefix}_${id}` : id;
|
||||
}
|
||||
|
||||
export function ThirdwebIcon(props: {
|
||||
className?: string;
|
||||
isMonoChrome?: boolean;
|
||||
}) {
|
||||
const id = useId();
|
||||
return (
|
||||
<svg className={props.className} fill="none" viewBox="0 0 516 321">
|
||||
<title>thirdweb</title>
|
||||
<g clipPath={`url(#${prefixId("clip0_3:35)", id)}`}>
|
||||
<path
|
||||
d="M1.40497 27.0011C-3.73633 14.022 5.84519 0 19.8669 0H106.919C115.098 0 122.342 4.86715 125.381 12.3996L194.671 185.299C196.541 189.935 196.541 195.149 194.671 199.901L151.087 308.484C144.427 325.056 120.823 325.056 114.163 308.484L1.40497 27.0011Z"
|
||||
fill={
|
||||
props.isMonoChrome
|
||||
? "currentColor"
|
||||
: `url(#${prefixId("paint0_linear_3:35)", id)}`
|
||||
}
|
||||
/>
|
||||
<path
|
||||
d="M169.547 26.4217C164.873 13.5585 174.454 0 188.242 0H264.077C272.49 0 279.968 5.2148 282.772 12.9791L345.753 185.879C347.272 190.166 347.272 194.918 345.753 199.321L307.894 303.27C301.585 320.652 276.813 320.652 270.503 303.27L169.547 26.4217Z"
|
||||
fill={
|
||||
props.isMonoChrome
|
||||
? "currentColor"
|
||||
: `url(#${prefixId("paint1_linear_3:35)", id)}`
|
||||
}
|
||||
/>
|
||||
<path
|
||||
d="M321.331 27.0011C316.19 14.022 325.771 0 339.793 0H426.845C435.024 0 442.269 4.86715 445.307 12.3996L514.597 185.299C516.467 189.935 516.467 195.149 514.597 199.901L471.013 308.484C464.353 325.056 440.75 325.056 434.089 308.484L321.331 27.0011Z"
|
||||
fill={
|
||||
props.isMonoChrome
|
||||
? "currentColor"
|
||||
: `url(#${prefixId("paint2_linear_3:35)", id)}`
|
||||
}
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id={prefixId("paint0_linear_3:35", id)}
|
||||
x1="7.40492"
|
||||
x2="260.485"
|
||||
y1="55.24"
|
||||
y2="164.437"
|
||||
>
|
||||
<stop stopColor="#F213A4" />
|
||||
<stop offset="0.1517" stopColor="#E011A7" />
|
||||
<stop offset="0.4554" stopColor="#B20DAF" />
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset="1" stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id={prefixId("paint1_linear_3:35", id)}
|
||||
x1="175.093"
|
||||
x2="410.968"
|
||||
y1="54.447"
|
||||
y2="148.471"
|
||||
>
|
||||
<stop stopColor="#F213A4" />
|
||||
<stop offset="0.1517" stopColor="#E011A7" />
|
||||
<stop offset="0.4554" stopColor="#B20DAF" />
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset="1" stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id={prefixId("paint2_linear_3:35", id)}
|
||||
x1="327.331"
|
||||
x2="580.411"
|
||||
y1="55.24"
|
||||
y2="164.437"
|
||||
>
|
||||
<stop stopColor="#F213A4" />
|
||||
<stop offset="0.1517" stopColor="#E011A7" />
|
||||
<stop offset="0.4554" stopColor="#B20DAF" />
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset="1" stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
<clipPath id={prefixId("clip0_3:35", id)}>
|
||||
<rect fill="white" height="321" width="516" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export function TokenIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="presentation"
|
||||
className={props.className}
|
||||
>
|
||||
<path
|
||||
d="M15.9377 15.9377C19.3603 15.4795 22 12.548 22 9C22 5.13401 18.866 2 15 2C11.452 2 8.52049 4.63967 8.06227 8.06227M16 15C16 18.866 12.866 22 9 22C5.13401 22 2 18.866 2 15C2 11.134 5.13401 8 9 8C12.866 8 16 11.134 16 15Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export function WalletProductIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="presentation"
|
||||
className={props.className}
|
||||
>
|
||||
<path
|
||||
d="M16 7.99995V4.5006C16 3.66887 16 3.253 15.8248 2.99742C15.6717 2.77413 15.4346 2.62245 15.1678 2.57703C14.8623 2.52505 14.4847 2.69932 13.7295 3.04787L4.85901 7.14194C4.18551 7.45279 3.84875 7.60822 3.60211 7.84927C3.38406 8.06237 3.21762 8.3225 3.1155 8.60978C3 8.93474 3 9.30563 3 10.0474V14.9999M16.5 14.4999H16.51M3 11.2L3 17.7999C3 18.9201 3 19.4801 3.21799 19.9079C3.40973 20.2843 3.71569 20.5902 4.09202 20.782C4.51984 20.9999 5.07989 20.9999 6.2 20.9999H17.8C18.9201 20.9999 19.4802 21 19.908 20.782C20.2843 20.5902 20.5903 20.2843 20.782 19.9079C21 19.4801 21 18.9201 21 17.7999V11.2C21 10.0798 21 9.51979 20.782 9.09197C20.5903 8.71565 20.2843 8.40969 19.908 8.21794C19.4802 7.99995 18.9201 7.99995 17.8 7.99995L6.2 7.99995C5.0799 7.99995 4.51984 7.99995 4.09202 8.21794C3.7157 8.40968 3.40973 8.71564 3.21799 9.09197C3 9.51979 3 10.0798 3 11.2ZM17 14.4999C17 14.7761 16.7761 14.9999 16.5 14.9999C16.2239 14.9999 16 14.7761 16 14.4999C16 14.2238 16.2239 13.9999 16.5 13.9999C16.7761 13.9999 17 14.2238 17 14.4999Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,51 @@
|
||||
export const isProd =
|
||||
(process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_VERCEL_ENV) ===
|
||||
"production";
|
||||
|
||||
function getVercelEnv(): "production" | "preview" | "development" {
|
||||
const onVercel = process.env.VERCEL || process.env.NEXT_PUBLIC_VERCEL_ENV;
|
||||
|
||||
// localhost
|
||||
if (!onVercel) {
|
||||
return "development";
|
||||
}
|
||||
|
||||
const vercelEnv =
|
||||
process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_VERCEL_ENV;
|
||||
|
||||
if (
|
||||
vercelEnv === "production" ||
|
||||
vercelEnv === "preview" ||
|
||||
vercelEnv === "development"
|
||||
) {
|
||||
return vercelEnv;
|
||||
}
|
||||
|
||||
// if running on vercel but for some reason failed to vercel env is not set -> production
|
||||
return "production";
|
||||
}
|
||||
|
||||
export function getBaseUrl(): string {
|
||||
const env = getVercelEnv();
|
||||
|
||||
// dev
|
||||
if (env === "development") {
|
||||
return "http://localhost:3000";
|
||||
}
|
||||
|
||||
// prod
|
||||
if (env === "production") {
|
||||
return "https://playground.thirdweb.com";
|
||||
}
|
||||
|
||||
// preview
|
||||
const vercelUrl =
|
||||
process.env.VERCEL_URL || process.env.NEXT_PUBLIC_VERCEL_URL;
|
||||
|
||||
if (vercelUrl) {
|
||||
return `https://${vercelUrl}`;
|
||||
}
|
||||
|
||||
// preview but missing the preview URL for some reason -> production
|
||||
return "https://playground.thirdweb.com";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getBaseUrl } from "./env";
|
||||
|
||||
const BASE_URL = getBaseUrl();
|
||||
|
||||
type DynamicImageOptions = {
|
||||
title: string;
|
||||
icon: "wallets" | "transactions" | "payments" | "insight" | "contract";
|
||||
};
|
||||
|
||||
export function createMetadata(obj: {
|
||||
title: string;
|
||||
description: string;
|
||||
image?: DynamicImageOptions;
|
||||
}): Metadata {
|
||||
const metaTitle = `${obj.title} | thirdweb Playground`;
|
||||
|
||||
return {
|
||||
description: obj.description,
|
||||
metadataBase: new URL(BASE_URL),
|
||||
openGraph: {
|
||||
description: obj.description,
|
||||
images: obj.image
|
||||
? [
|
||||
{
|
||||
height: 630,
|
||||
url: `${BASE_URL}/api/og?icon=${obj.image.icon}&title=${obj.image.title}`,
|
||||
width: 1200,
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
locale: "en_US",
|
||||
siteName: "thirdweb Playground",
|
||||
title: metaTitle,
|
||||
type: "website",
|
||||
},
|
||||
title: metaTitle,
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
creator: "@thirdweb",
|
||||
description: obj.description,
|
||||
site: "@thirdweb",
|
||||
title: metaTitle,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -75,6 +75,16 @@ module.exports = {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
sidebar: {
|
||||
accent: "hsl(var(--sidebar-accent))",
|
||||
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
|
||||
border: "hsl(var(--sidebar-border))",
|
||||
DEFAULT: "hsl(var(--sidebar-background))",
|
||||
foreground: "hsl(var(--sidebar-foreground))",
|
||||
primary: "hsl(var(--sidebar-primary))",
|
||||
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
|
||||
ring: "hsl(var(--sidebar-ring))",
|
||||
},
|
||||
success: {
|
||||
text: "hsl(var(--success-text))",
|
||||
},
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { BotIcon, WebhookIcon, ZapIcon } from "lucide-react";
|
||||
import { ArrowUpRightIcon, BotIcon, WebhookIcon, ZapIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Heading } from "@/components/Document";
|
||||
import { ChatButton } from "../components/AI/chat-button";
|
||||
import { Button } from "../components/ui/button";
|
||||
import {
|
||||
DotNetIcon,
|
||||
ExternalLinkIcon,
|
||||
@@ -44,8 +45,18 @@ function Hero() {
|
||||
<p className="mb-8 max-w-md text-lg text-muted-foreground leading-normal">
|
||||
Development framework for building onchain apps, games, and agents.
|
||||
</p>
|
||||
<div className="flex">
|
||||
<div className="flex gap-3">
|
||||
<ChatButton />
|
||||
<Button
|
||||
asChild
|
||||
className="flex items-center gap-2 rounded-full bg-card"
|
||||
variant="outline"
|
||||
>
|
||||
<Link href="https://playground.thirdweb.com">
|
||||
Playground
|
||||
<ArrowUpRightIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -250,7 +261,7 @@ function SDKCard(props: {
|
||||
<props.icon className="size-4 shrink-0" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<h3 className="font-medium text-base text-foreground mb-0.5">
|
||||
<h3 className="font-semibold text-base text-foreground mb-0.5">
|
||||
<Link
|
||||
href={props.href}
|
||||
target={props.href.includes("http") ? "_blank" : undefined}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
function getVercelEnv(): "production" | "preview" | "development" {
|
||||
const onVercel = process.env.vercel || process.env.NEXT_PUBLIC_VERCEL_ENV;
|
||||
const onVercel = process.env.VERCEL || process.env.NEXT_PUBLIC_VERCEL_ENV;
|
||||
|
||||
// localhost
|
||||
if (!onVercel) {
|
||||
|
||||