Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdb4c31a6a | ||
|
|
171594d66a | ||
|
|
760b250bc3 | ||
|
|
669a95f983 | ||
|
|
928113bb0b | ||
|
|
c8b8b2faad | ||
|
|
9d40b13299 | ||
|
|
bda0f3edb8 | ||
|
|
dfa3ee0231 | ||
|
|
3a100c98ef | ||
|
|
08cff4b6c2 | ||
|
|
f2b4fe6727 | ||
|
|
6dd2b09450 | ||
|
|
736c3f8558 | ||
|
|
055e451fd0 | ||
|
|
2c6bd620b5 | ||
|
|
98e0cbdc71 | ||
|
|
1bc036ce6f | ||
|
|
9f90b87c31 | ||
|
|
e9d0b6e3d2 | ||
|
|
ee065b8d10 | ||
|
|
b17078b025 | ||
|
|
a4c9b28149 | ||
|
|
96155016d1 | ||
|
|
b08efa1778 | ||
|
|
6745ba092c | ||
|
|
80bf795760 | ||
|
|
7ae174fed5 | ||
|
|
c803893a17 | ||
|
|
4839173d00 | ||
|
|
1e82083a3c | ||
|
|
20d182b228 | ||
|
|
a96fa915cf | ||
|
|
4b58eca7c8 | ||
|
|
6e87370687 | ||
|
|
465cf8c990 | ||
|
|
98cce77cf1 | ||
|
|
5f0049f35f | ||
|
|
9a63cd4b57 | ||
|
|
004f3776cf |
@@ -9,6 +9,12 @@
|
||||
"typescript.preferences.autoImportFileExcludePatterns": [
|
||||
"./packages/thirdweb/src/exports"
|
||||
],
|
||||
"typescript.preferences.autoImportSpecifierExcludeRegexes": [
|
||||
"@radix-ui",
|
||||
"next/router",
|
||||
"next/dist",
|
||||
"^lucide-react/dist/lucide-react.suffixed$"
|
||||
],
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
declare module "lucide-react" {
|
||||
export * from "lucide-react/dist/lucide-react.suffixed";
|
||||
}
|
||||
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 228 KiB |
|
Before Width: | Height: | Size: 281 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,6 +0,0 @@
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Check Circle Icon">
|
||||
<path id="Vector" d="M8.5 16.5C12.9184 16.5 16.5 12.9184 16.5 8.5C16.5 4.0816 12.9184 0.5 8.5 0.5C4.0816 0.5 0.5 4.0816 0.5 8.5C0.5 12.9184 4.0816 16.5 8.5 16.5Z" fill="white"/>
|
||||
<path id="Check" d="M4.87939 9.01716L6.9482 11.086L12.1202 5.91394" stroke="#000001" stroke-width="1.23077" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 470 B |
|
Before Width: | Height: | Size: 395 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 244 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 314 KiB |
|
Before Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 676 KiB |
|
Before Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 779 KiB |
|
Before Width: | Height: | Size: 269 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 5.5 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 97 KiB |
@@ -100,11 +100,6 @@ async function redirects() {
|
||||
destination: "/auth",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/checkout",
|
||||
destination: "/connect",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/extensions",
|
||||
destination: "/build",
|
||||
|
||||
@@ -88,7 +88,7 @@ function main(params: { assetsFolder: string; srcFolder: string }) {
|
||||
// remove the files for which its name can be found in the content of the file
|
||||
folderVisitor(params.srcFolder, (content) => {
|
||||
for (const assetFileName of assetFileNames) {
|
||||
if (content.includes(assetFileName.name)) {
|
||||
if (content.includes(`/${assetFileName.name}`)) {
|
||||
unusedFileNames.delete(assetFileName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import "server-only";
|
||||
import { API_SERVER_URL, THIRDWEB_API_SECRET } from "@/constants/env";
|
||||
import type { TeamResponse } from "@thirdweb-dev/service-utils";
|
||||
import { cookies } from "next/headers";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { LAST_USED_TEAM_ID } from "../../constants/cookies";
|
||||
|
||||
export type Team = TeamResponse & { stripeCustomerId: string | null };
|
||||
|
||||
@@ -74,3 +76,22 @@ export async function getDefaultTeam() {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getLastVisitedTeamOrDefaultTeam() {
|
||||
const token = await getAuthToken();
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cookiesStore = await cookies();
|
||||
const lastVisitedTeamId = cookiesStore.get(LAST_USED_TEAM_ID)?.value;
|
||||
|
||||
if (lastVisitedTeamId) {
|
||||
const team = await getTeamById(lastVisitedTeamId);
|
||||
if (team) {
|
||||
return team;
|
||||
}
|
||||
}
|
||||
|
||||
return getDefaultTeam();
|
||||
}
|
||||
|
||||
@@ -100,12 +100,14 @@ export type Fee = {
|
||||
|
||||
export async function getFees(props: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/fees`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id-override": props.clientId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
@@ -122,6 +124,7 @@ export async function getFees(props: {
|
||||
|
||||
export async function updateFee(props: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
feeRecipient: string;
|
||||
feeBps: number;
|
||||
}) {
|
||||
@@ -131,6 +134,7 @@ export async function updateFee(props: {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id-override": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "@/components/ui/popover";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckIcon, ChevronDown, SearchIcon, XIcon } from "lucide-react";
|
||||
import { CheckIcon, ChevronDownIcon, SearchIcon, XIcon } from "lucide-react";
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
@@ -201,7 +201,7 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
orientation="vertical"
|
||||
className="flex h-full min-h-6"
|
||||
/>
|
||||
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
|
||||
<ChevronDownIcon className="h-4 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -209,7 +209,7 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{placeholder}
|
||||
</span>
|
||||
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
|
||||
<ChevronDownIcon className="h-4 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckIcon, ChevronDown, SearchIcon } from "lucide-react";
|
||||
import { CheckIcon, ChevronDownIcon, SearchIcon } from "lucide-react";
|
||||
import React, { useRef, useMemo, useEffect } from "react";
|
||||
import { useShowMore } from "../../lib/useShowMore";
|
||||
import { ScrollShadow } from "../ui/ScrollShadow/ScrollShadow";
|
||||
@@ -131,7 +131,7 @@ export const SelectWithSearch = React.forwardRef<
|
||||
>
|
||||
{selectedOption?.label || placeholder}
|
||||
</span>
|
||||
<ChevronDown className="size-4 cursor-pointer text-muted-foreground" />
|
||||
<ChevronDownIcon className="size-4 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { Check, Copy, XIcon } from "lucide-react";
|
||||
import { CheckIcon, CopyIcon, XIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { type ThirdwebClient, isAddress } from "thirdweb";
|
||||
import { ZERO_ADDRESS } from "thirdweb";
|
||||
@@ -104,9 +104,9 @@ export function WalletAddress(props: {
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{hasCopied ? (
|
||||
<Check className="h-4 w-4" />
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
)}
|
||||
{hasCopied ? "Copied!" : "Copy"}
|
||||
</Button>
|
||||
|
||||
@@ -9,11 +9,7 @@ import {
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { format, isBefore } from "date-fns";
|
||||
import {
|
||||
Calendar as CalendarIcon,
|
||||
CalendarX2Icon,
|
||||
ChevronDownIcon,
|
||||
} from "lucide-react";
|
||||
import { CalendarIcon, CalendarX2Icon, ChevronDownIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { DynamicHeight } from "./DynamicHeight";
|
||||
import { TabButtons } from "./tabs";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -34,7 +34,7 @@ const AccordionTrigger = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
<ChevronDownIcon className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -84,7 +84,7 @@ const BreadcrumbSeparator = ({
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
{children ?? <ChevronRightIcon />}
|
||||
</li>
|
||||
);
|
||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
|
||||
@@ -102,7 +102,7 @@ const BreadcrumbEllipsis = ({
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<MoreHorizontalIcon className="h-4 w-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import type * as React from "react";
|
||||
import { DayPicker } from "react-day-picker";
|
||||
|
||||
@@ -54,8 +54,8 @@ function Calendar({
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: () => <ChevronLeft className="h-4 w-4" />,
|
||||
IconRight: () => <ChevronRight className="h-4 w-4" />,
|
||||
IconLeft: () => <ChevronLeftIcon className="size-4" />,
|
||||
IconRight: () => <ChevronRightIcon className="size-4" />,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { Check } from "lucide-react";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -21,7 +21,7 @@ const Checkbox = React.forwardRef<
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
<CheckIcon className="size-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import { XIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -65,7 +65,7 @@ const DialogContent = React.forwardRef<
|
||||
dialogCloseClassName,
|
||||
)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -34,7 +34,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
@@ -107,7 +107,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
@@ -130,7 +130,7 @@ const DropdownMenuRadioItem = React.forwardRef<
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { OTPInput, OTPInputContext } from "input-otp";
|
||||
import { Dot } from "lucide-react";
|
||||
import { DotIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -65,7 +65,7 @@ const InputOTPSeparator = React.forwardRef<
|
||||
>(({ ...props }, ref) => (
|
||||
// biome-ignore lint/a11y/useFocusableInteractive: pure shadcn component - it works
|
||||
<div ref={ref} role="separator" {...props}>
|
||||
<Dot />
|
||||
<DotIcon />
|
||||
</div>
|
||||
));
|
||||
InputOTPSeparator.displayName = "InputOTPSeparator";
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
MoreHorizontalIcon,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { Button, type ButtonProps } from "@/components/ui/button";
|
||||
@@ -66,7 +70,7 @@ const PaginationPrevious = ({
|
||||
className={cn("h-10 gap-1 pl-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
<ChevronLeftIcon className="size-4" />
|
||||
<span className="max-sm:hidden">Previous</span>
|
||||
</PaginationLink>
|
||||
);
|
||||
@@ -83,7 +87,7 @@ const PaginationNext = ({
|
||||
{...props}
|
||||
>
|
||||
<span className="max-sm:hidden">Next</span>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</PaginationLink>
|
||||
);
|
||||
PaginationNext.displayName = "PaginationNext";
|
||||
@@ -100,7 +104,7 @@ const PaginationEllipsis = ({
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
import { Circle } from "lucide-react";
|
||||
import { CircleIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -35,7 +35,7 @@ const RadioGroupItem = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
||||
<Circle className="h-2.5 w-2.5 fill-current text-current" />
|
||||
<CircleIcon className="size-2.5 fill-current text-current" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
@@ -59,11 +59,11 @@ const RadioGroupItemButton = React.forwardRef<
|
||||
<div className="flex aspect-square h-4 w-4 items-center justify-center rounded-full border text-inverted ring-offset-background transition-all focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 group-hover:border-foreground/25 group-data-[state=checked]:border-inverted">
|
||||
{/* Show on checked */}
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center rounded-full transition-all">
|
||||
<Circle className="h-2 w-2 fill-current text-current" />
|
||||
<CircleIcon className="size-2 fill-current text-current" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
{/* Show on hover */}
|
||||
<div className="hidden items-center justify-center rounded-full text-foreground/25 transition-all group-hover:flex group-data-[state=checked]:hidden">
|
||||
<Circle className="h-2 w-2 fill-current text-current" />
|
||||
<CircleIcon className="size-2 fill-current text-current" />
|
||||
</div>
|
||||
</div>
|
||||
<Label className="cursor-pointer">{props.children}</Label>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown } from "lucide-react";
|
||||
import { CheckIcon, ChevronDownIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -28,7 +28,7 @@ const SelectTrigger = React.forwardRef<
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className={cn("h-4 w-4 opacity-50", chevronClassName)} />
|
||||
<ChevronDownIcon className={cn("h-4 w-4 opacity-50", chevronClassName)} />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
@@ -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>
|
||||
<Check className="h-4 w-4" />
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
import { XIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -66,7 +66,7 @@ const SheetContent = React.forwardRef<
|
||||
>
|
||||
{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">
|
||||
<X className="h-4 w-4" />
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
|
||||
@@ -21,7 +21,7 @@ import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import { PanelLeft } from "lucide-react";
|
||||
import { PanelLeftIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||
@@ -288,7 +288,7 @@ const SidebarTrigger = React.forwardRef<
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<PanelLeft />
|
||||
<PanelLeftIcon />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { FileText } from "lucide-react";
|
||||
import { FileTextIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { SectionTitle } from "../server/SectionTitle";
|
||||
@@ -14,7 +14,7 @@ export default function NextSteps(props: { chain: ChainMetadata }) {
|
||||
<SectionTitle title="Next Steps" />
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||
<div className="relative flex gap-3 rounded-lg border bg-card p-4 pr-8 transition-colors hover:border-active-border">
|
||||
<FileText className="mt-0.5 size-5 shrink-0" />
|
||||
<FileTextIcon className="mt-0.5 size-5 shrink-0" />
|
||||
<div>
|
||||
<h3 className="mb-1.5 font-medium">
|
||||
<Link
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { DASHBOARD_THIRDWEB_CLIENT_ID, isProd } from "@/constants/env";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { CircleCheck, XIcon } from "lucide-react";
|
||||
import { CircleCheckIcon, XIcon } from "lucide-react";
|
||||
import { hostnameEndsWith } from "utils/url";
|
||||
import { PrimaryInfoItem } from "../server/primary-info-item";
|
||||
|
||||
@@ -67,7 +67,7 @@ export function ChainLiveStats(props: { rpc: string }) {
|
||||
titleIcon={
|
||||
stats.isSuccess ? (
|
||||
<ToolTipLabel label="Working">
|
||||
<CircleCheck className="size-4 text-success-text" />
|
||||
<CircleCheckIcon className="size-4 text-success-text" />
|
||||
</ToolTipLabel>
|
||||
) : stats.isError ? (
|
||||
<ToolTipLabel label="Not Working">
|
||||
|
||||
@@ -13,11 +13,11 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { ChevronDownIcon, TicketCheckIcon } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getThirdwebClient } from "../../../../../../@/constants/thirdweb.server";
|
||||
import { mapV4ChainToV5Chain } from "../../../../../../contexts/map-chains";
|
||||
import { NebulaChatButton } from "../../../../../nebula-app/(app)/components/FloatingChat/FloatingChat";
|
||||
import {
|
||||
@@ -100,10 +100,10 @@ The following is the user's message:
|
||||
return (
|
||||
<>
|
||||
<NebulaChatButton
|
||||
isLoggedIn={!!authToken}
|
||||
networks={chain.testnet ? "testnet" : "mainnet"}
|
||||
isFloating={true}
|
||||
pageType="chain"
|
||||
authToken={authToken ?? undefined}
|
||||
label="Ask AI about this chain"
|
||||
client={client}
|
||||
nebulaParams={{
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { mapV4ChainToV5Chain } from "contexts/map-chains";
|
||||
import { cookies } from "next/headers";
|
||||
import { TW_LOCAL_CHAIN_STORE } from "stores/storageKeys";
|
||||
import { getAddress, getContract, isAddress } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { fetchChain } from "utils/fetchChain";
|
||||
import { LAST_USED_TEAM_ID } from "../../../../../../../constants/cookies";
|
||||
import { fetchChainWithLocalOverrides } from "../../../../../../../utils/fetchChainWithLocalOverrides";
|
||||
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
|
||||
|
||||
export async function getContractPageParamsInfo(params: {
|
||||
@@ -14,29 +12,7 @@ export async function getContractPageParamsInfo(params: {
|
||||
}) {
|
||||
const contractAddress = getCheckSummedAddress(params.contractAddress);
|
||||
const chainSlugOrId = params.chain_id;
|
||||
let chainMetadata = await fetchChain(chainSlugOrId).catch(() => null);
|
||||
|
||||
const cookieStore = await cookies();
|
||||
const localChainStoreValue = cookieStore.get(TW_LOCAL_CHAIN_STORE)?.value;
|
||||
|
||||
if (localChainStoreValue) {
|
||||
try {
|
||||
const chains = JSON.parse(decodeURIComponent(localChainStoreValue));
|
||||
if (typeof chains === "object" && Array.isArray(chains)) {
|
||||
const chainOverrides = chains as ChainMetadata[];
|
||||
const chain = chainOverrides.find(
|
||||
(c) =>
|
||||
c.slug === chainSlugOrId ||
|
||||
c.chainId === Number.parseInt(chainSlugOrId),
|
||||
);
|
||||
if (chain) {
|
||||
chainMetadata = chain;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
const chainMetadata = await fetchChainWithLocalOverrides(chainSlugOrId);
|
||||
|
||||
if (!chainMetadata) {
|
||||
return undefined;
|
||||
|
||||
@@ -10,7 +10,6 @@ import { isAddress, isContractDeployed } from "thirdweb/utils";
|
||||
import { shortenIfAddress } from "utils/usedapp-external";
|
||||
import { NebulaChatButton } from "../../../../../nebula-app/(app)/components/FloatingChat/FloatingChat";
|
||||
import {
|
||||
getAuthToken,
|
||||
getAuthTokenWalletAddress,
|
||||
getUserThirdwebClient,
|
||||
} from "../../../../api/lib/getAuthToken";
|
||||
@@ -47,10 +46,7 @@ export default async function Layout(props: {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const [authToken, accountAddress] = await Promise.all([
|
||||
getAuthToken(),
|
||||
getAuthTokenWalletAddress(),
|
||||
]);
|
||||
const accountAddress = await getAuthTokenWalletAddress();
|
||||
|
||||
const client = await getUserThirdwebClient();
|
||||
const teamsAndProjects = await getTeamsAndProjectsIfLoggedIn();
|
||||
@@ -115,10 +111,10 @@ The following is the user's message:`;
|
||||
client={client}
|
||||
>
|
||||
<NebulaChatButton
|
||||
isLoggedIn={!!accountAddress}
|
||||
networks={info.chainMetadata.testnet ? "testnet" : "mainnet"}
|
||||
isFloating={true}
|
||||
pageType="contract"
|
||||
authToken={authToken ?? undefined}
|
||||
label="Ask AI about this contract"
|
||||
client={client}
|
||||
nebulaParams={{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { Alert, AlertTitle } from "@/components/ui/alert";
|
||||
import { CircleSlash } from "lucide-react";
|
||||
import { CircleSlashIcon } from "lucide-react";
|
||||
import type { ThirdwebContract } from "thirdweb/contract";
|
||||
import type { Account } from "thirdweb/wallets";
|
||||
import { ModuleCard } from "./module-card";
|
||||
@@ -37,7 +37,7 @@ export const InstalledModulesTable = (props: {
|
||||
{sectionTitle}
|
||||
<Alert variant="destructive">
|
||||
<div className="flex items-center gap-3">
|
||||
<CircleSlash className="size-6 text-red-400" />
|
||||
<CircleSlashIcon className="size-6 text-red-400" />
|
||||
<AlertTitle className="mb-0">No modules installed</AlertTitle>
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { CircleCheck, Upload } from "lucide-react";
|
||||
import { CircleCheckIcon, UploadIcon } from "lucide-react";
|
||||
import { type Dispatch, type SetStateAction, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
@@ -108,13 +108,13 @@ export const TokenAirdropForm: React.FC<TokenAirdropFormProps> = ({
|
||||
colorScheme="primary"
|
||||
borderRadius="md"
|
||||
onClick={() => setAirdropFormOpen(true)}
|
||||
rightIcon={<Upload size={16} />}
|
||||
rightIcon={<UploadIcon className="size-4" />}
|
||||
>
|
||||
Upload addresses
|
||||
</Button>
|
||||
{addresses.length > 0 && (
|
||||
<div className="flex flex-row items-center justify-center gap-2 text-green-500">
|
||||
<CircleCheck className="text-green-500" size={16} />
|
||||
<CircleCheckIcon className="text-green-500" size={16} />
|
||||
<Text size="body.sm" color="inherit">
|
||||
<strong>{addresses.length} addresses</strong> ready to be
|
||||
airdropped
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { FormControl, Input } from "@chakra-ui/react";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { Flame } from "lucide-react";
|
||||
import { FlameIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -66,7 +66,7 @@ export const TokenBurnButton: React.FC<TokenBurnButtonProps> = ({
|
||||
disabled={!hasBalance}
|
||||
className="gap-2"
|
||||
>
|
||||
<Flame size={16} /> Burn
|
||||
<FlameIcon className="size-4" /> Burn
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { ChevronDownIcon, Filter, XIcon } from "lucide-react";
|
||||
import { ChevronDownIcon, FilterIcon, XIcon } from "lucide-react";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import type React from "react";
|
||||
import { type PropsWithChildren, useCallback, useMemo } from "react";
|
||||
@@ -32,7 +32,7 @@ export function AllFilters(props: { hideChainType?: boolean }) {
|
||||
variant="outline"
|
||||
className="h-10 w-auto gap-2 p-2 lg:h-10 lg:border-0 lg:px-4 lg:py-2"
|
||||
>
|
||||
<Filter strokeWidth={1} className="lg:size-4" />
|
||||
<FilterIcon strokeWidth={1} className="lg:size-4" />
|
||||
<span className="hidden lg:inline">All Filters</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Button, type ButtonProps } from "@/components/ui/button";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Star } from "lucide-react";
|
||||
import { StarIcon } from "lucide-react";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
|
||||
async function favoriteChains() {
|
||||
@@ -104,7 +104,7 @@ export function StarButton(props: {
|
||||
{mutation.isPending ? (
|
||||
<Spinner className={cn("size-6", props.iconClassName)} />
|
||||
) : (
|
||||
<Star
|
||||
<StarIcon
|
||||
className={cn(
|
||||
"size-6 text-foreground transition-all",
|
||||
props.iconClassName,
|
||||
|
||||
@@ -158,10 +158,10 @@ export default async function SupportPage() {
|
||||
</p>
|
||||
<div className="mt-6 flex w-full flex-col items-center gap-3">
|
||||
<NebulaChatButton
|
||||
isLoggedIn={!!accountAddress}
|
||||
networks="all"
|
||||
isFloating={false}
|
||||
pageType="support"
|
||||
authToken={authToken ?? undefined}
|
||||
label="Ask Nebula AI for support"
|
||||
client={client}
|
||||
nebulaParams={{
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import type { Abi, AbiFunction } from "abitype";
|
||||
import { useV5DashboardChain } from "lib/v5-adapter";
|
||||
import { ArrowDown, WalletIcon } from "lucide-react";
|
||||
import { ArrowDownIcon, WalletIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
@@ -207,7 +207,7 @@ ${Object.keys(populatedTransaction)
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<ArrowDown />
|
||||
<ArrowDownIcon className="size-4" />
|
||||
</div>
|
||||
|
||||
<Card className="flex flex-col gap-4 p-4">
|
||||
@@ -273,7 +273,7 @@ ${Object.keys(populatedTransaction)
|
||||
{state.message && (
|
||||
<>
|
||||
<div className="flex justify-center">
|
||||
<ArrowDown />
|
||||
<ArrowDownIcon className="size-4" />
|
||||
</div>
|
||||
<Card className="max-w-[800px] p-4">
|
||||
<p className="overflow-auto whitespace-pre-wrap font-mono text-sm">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fetchChain } from "utils/fetchChain";
|
||||
import { fetchChainWithLocalOverrides } from "../../../../../utils/fetchChainWithLocalOverrides";
|
||||
import {
|
||||
type ProjectContract,
|
||||
getProjectContracts,
|
||||
@@ -19,7 +19,9 @@ export async function getSortedDeployedContracts(params: {
|
||||
const chainIds = Array.from(new Set(contracts.map((c) => c.chainId)));
|
||||
const chains = (
|
||||
await Promise.allSettled(
|
||||
chainIds.map((chainId) => fetchChain(chainId.toString())),
|
||||
chainIds.map((chainId) =>
|
||||
fetchChainWithLocalOverrides(chainId.toString()),
|
||||
),
|
||||
)
|
||||
)
|
||||
.filter((c) => c.status === "fulfilled")
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useEns } from "components/contract-components/hooks";
|
||||
import { LogOutIcon, Moon, Sun } from "lucide-react";
|
||||
import { LogOutIcon, MoonIcon, SunIcon } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
@@ -107,13 +107,13 @@ export function AccountButton(props: {
|
||||
<p className="text-sm">Theme</p>
|
||||
|
||||
<div className="ml-auto flex items-center gap-2 rounded-lg border border-border px-3 py-1.5 text-foreground">
|
||||
<Sun
|
||||
<SunIcon
|
||||
className={cn(
|
||||
"size-4",
|
||||
theme === "light" ? "opacity-100" : "opacity-30",
|
||||
)}
|
||||
/>
|
||||
<Moon
|
||||
<MoonIcon
|
||||
className={cn(
|
||||
"size-4",
|
||||
theme === "dark" ? "opacity-100" : "opacity-30",
|
||||
|
||||
@@ -8,8 +8,8 @@ import { useEns } from "components/contract-components/hooks";
|
||||
import {
|
||||
LogOutIcon,
|
||||
MenuIcon,
|
||||
Moon,
|
||||
Sun,
|
||||
MoonIcon,
|
||||
SunIcon,
|
||||
UserRoundIcon,
|
||||
WalletIcon,
|
||||
XIcon,
|
||||
@@ -213,7 +213,7 @@ export function MobileBurgerMenuButton(
|
||||
>
|
||||
<span className="text-base text-muted-foreground">Theme</span>
|
||||
<div className="ml-auto flex items-center gap-2 rounded-lg border px-2 py-1">
|
||||
<Sun
|
||||
<SunIcon
|
||||
className={cn(
|
||||
"size-4",
|
||||
theme === "light"
|
||||
@@ -221,7 +221,7 @@ export function MobileBurgerMenuButton(
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
/>
|
||||
<Moon
|
||||
<MoonIcon
|
||||
className={cn(
|
||||
"size-4",
|
||||
theme === "dark"
|
||||
|
||||
@@ -20,7 +20,12 @@ import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { z } from "zod";
|
||||
import { teamSlugRegex } from "../../../team/[team_slug]/(team)/~/settings/general/common";
|
||||
import {
|
||||
maxTeamNameLength,
|
||||
maxTeamSlugLength,
|
||||
teamNameSchema,
|
||||
teamSlugSchema,
|
||||
} from "../../../team/[team_slug]/(team)/~/settings/general/common";
|
||||
|
||||
type TeamData = {
|
||||
name?: string;
|
||||
@@ -28,27 +33,8 @@ type TeamData = {
|
||||
image?: File;
|
||||
};
|
||||
|
||||
const teamSlugSchema = z
|
||||
.string()
|
||||
.min(3, {
|
||||
message: "URL must be at least 3 characters",
|
||||
})
|
||||
.max(48, {
|
||||
message: "URL must be at most 48 characters",
|
||||
})
|
||||
.refine((slug) => !teamSlugRegex.test(slug), {
|
||||
message: "URL can only contain lowercase letters, numbers and hyphens",
|
||||
});
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(3, {
|
||||
message: "Name must be at least 3 characters",
|
||||
})
|
||||
.max(32, {
|
||||
message: "Name must be at most 32 characters",
|
||||
}),
|
||||
name: teamNameSchema,
|
||||
slug: teamSlugSchema,
|
||||
image: z.instanceof(File).optional(),
|
||||
});
|
||||
@@ -155,8 +141,6 @@ export function TeamInfoFormUI(props: {
|
||||
});
|
||||
}
|
||||
|
||||
const maxTeamNameLength = 32;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-card ">
|
||||
<Form {...form}>
|
||||
@@ -234,6 +218,7 @@ export function TeamInfoFormUI(props: {
|
||||
}}
|
||||
className="truncate border-0 font-mono"
|
||||
placeholder="my-team"
|
||||
maxLength={maxTeamSlugLength}
|
||||
/>
|
||||
{(isCheckingSlug || isCalculatingSlug) && (
|
||||
<div className="-translate-y-1/2 fade-in-0 absolute top-1/2 right-3 duration-300">
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
|
||||
import { PROJECT_SHOWCASE_DATA } from "lib/project-showcase-constants";
|
||||
import { ExternalLink, FileText } from "lucide-react";
|
||||
import { ExternalLinkIcon, FileTextIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function DetailPage(props: {
|
||||
@@ -52,7 +52,7 @@ export default async function DetailPage(props: {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ExternalLink className="mr-2 h-4 w-4" />
|
||||
<ExternalLinkIcon className="mr-2 h-4 w-4" />
|
||||
Visit Project Website
|
||||
</Link>
|
||||
</Button>
|
||||
@@ -63,7 +63,7 @@ export default async function DetailPage(props: {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<FileText className="mr-2 h-4 w-4" />
|
||||
<FileTextIcon className="mr-2 h-4 w-4" />
|
||||
Link to Case Study
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
PROJECT_SHOWCASE_ITEMS_PER_PAGE,
|
||||
} from "lib/project-showcase-constants";
|
||||
import { getAbsoluteUrl } from "lib/vercel-utils";
|
||||
import { FileText } from "lucide-react";
|
||||
import { FileTextIcon } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
@@ -171,7 +171,7 @@ export default async function ProjectShowcasePage(props: {
|
||||
// onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Button variant="outline" size="sm">
|
||||
<FileText className="mr-2 h-4 w-4" /> Case Study
|
||||
<FileTextIcon className="mr-2 h-4 w-4" /> Case Study
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandl
|
||||
import {
|
||||
AlertTriangleIcon,
|
||||
CheckIcon,
|
||||
ChevronsUpDown,
|
||||
ChevronsUpDownIcon,
|
||||
ExternalLinkIcon,
|
||||
PlusCircleIcon,
|
||||
} from "lucide-react";
|
||||
@@ -78,7 +78,7 @@ function EcosystemSelect(props: {
|
||||
className="relative flex w-full justify-start truncate pr-8 pl-3 md:w-48"
|
||||
>
|
||||
<div className="truncate">{props.ecosystem?.name}</div>
|
||||
<ChevronsUpDown className="absolute right-2 h-4 w-4 text-muted-foreground" />
|
||||
<ChevronsUpDownIcon className="absolute right-2 h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-full md:w-48">
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Pencil, Trash2 } from "lucide-react";
|
||||
import { PencilIcon, Trash2Icon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Link } from "tw-components/link";
|
||||
import type { Ecosystem, Partner } from "../../../../../types";
|
||||
@@ -146,7 +146,7 @@ function PartnerRow(props: {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Pencil className="size-4" />
|
||||
<PencilIcon className="size-4" />
|
||||
<span className="sr-only">Edit</span>
|
||||
</Button>
|
||||
<ConfirmationDialog
|
||||
@@ -179,7 +179,7 @@ function PartnerRow(props: {
|
||||
className="text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
<Trash2Icon className="size-4" />
|
||||
<span className="sr-only">Delete</span>
|
||||
</Button>
|
||||
</ConfirmationDialog>
|
||||
|
||||
@@ -63,7 +63,7 @@ import {
|
||||
Trash2Icon,
|
||||
} from "lucide-react";
|
||||
import { MoreHorizontalIcon } from "lucide-react";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { ArrowRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -868,7 +868,7 @@ function EmptyEngineState(props: {
|
||||
>
|
||||
<Link href={`/team/${props.teamSlug}/~/engine/import`}>
|
||||
Import self-hosted Engine
|
||||
<ArrowRight size={16} />
|
||||
<ArrowRightIcon size={16} />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -902,14 +902,14 @@ function EmptyEngineState(props: {
|
||||
<Button className="w-full gap-2" size="sm" asChild>
|
||||
<Link href={`/team/${props.teamSlug}/~/settings/billing`}>
|
||||
Upgrade Plan
|
||||
<ArrowRight size={16} />
|
||||
<ArrowRightIcon size={16} />
|
||||
</Link>
|
||||
</Button>
|
||||
) : (
|
||||
<Button className="w-full gap-2" size="sm" asChild>
|
||||
<Link href={PRO_CONTACT_US_URL} target="_blank">
|
||||
Contact Us
|
||||
<ArrowRight size={16} />
|
||||
<ArrowRightIcon size={16} />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useEngineCreateWalletCredential } from "@3rdweb-sdk/react/hooks/useEngine";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { Plus } from "lucide-react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { CredentialForm } from "./credential-form";
|
||||
@@ -61,7 +61,7 @@ export const CreateWalletCredentialButton: React.FC<
|
||||
setIsOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
<PlusIcon className="mr-2 h-4 w-4" />
|
||||
Create Wallet Credential
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import { createColumnHelper } from "@tanstack/react-table";
|
||||
import { TWTable } from "components/shared/TWTable";
|
||||
import { format, formatDistanceToNowStrict } from "date-fns";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { MailQuestion, TrashIcon } from "lucide-react";
|
||||
import { MailQuestionIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Card, FormLabel, Text } from "tw-components";
|
||||
@@ -137,7 +137,7 @@ export const WebhooksTable: React.FC<WebhooksTableProps> = ({
|
||||
isFetched={isFetched}
|
||||
onMenuClick={[
|
||||
{
|
||||
icon: <MailQuestion className="size-4" />,
|
||||
icon: <MailQuestionIcon className="size-4" />,
|
||||
text: "Test webhook",
|
||||
onClick: (row) => {
|
||||
setSelectedWebhook(row);
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { AlertCircle, CheckCircle } from "lucide-react";
|
||||
import { AlertCircleIcon, CheckCircleIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface DomainVerificationFormProps {
|
||||
@@ -142,7 +142,7 @@ export function TeamDomainVerificationCard({
|
||||
</div>
|
||||
|
||||
<Alert variant="info">
|
||||
<AlertCircle className="size-4" />
|
||||
<AlertCircleIcon className="size-4" />
|
||||
<AlertTitle>
|
||||
DNS changes can take up to 48 hours to propagate.
|
||||
</AlertTitle>
|
||||
@@ -174,7 +174,7 @@ export function TeamDomainVerificationCard({
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="mt-2 flex items-start space-x-3">
|
||||
<CheckCircle className="mt-0.5 h-5 w-5 text-green-500" />
|
||||
<CheckCircleIcon className="mt-0.5 h-5 w-5 text-green-500" />
|
||||
<div>
|
||||
<p className="font-medium">{domainQuery.data.domain}</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
|
||||
@@ -8,13 +8,21 @@ import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { FileInput } from "components/shared/FileInput";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { z } from "zod";
|
||||
import { TeamDomainVerificationCard } from "../_components/settings-cards/domain-verification";
|
||||
import { teamSlugRegex } from "./common";
|
||||
import {
|
||||
maxTeamNameLength,
|
||||
maxTeamSlugLength,
|
||||
teamNameSchema,
|
||||
teamSlugSchema,
|
||||
} from "./common";
|
||||
|
||||
type UpdateTeamField = (team: Partial<Team>) => Promise<void>;
|
||||
|
||||
@@ -59,111 +67,109 @@ export function TeamGeneralSettingsPageUI(props: {
|
||||
);
|
||||
}
|
||||
|
||||
const teamNameFormSchema = z.object({
|
||||
name: teamNameSchema,
|
||||
});
|
||||
|
||||
function TeamNameFormControl(props: {
|
||||
team: Team;
|
||||
updateTeamField: UpdateTeamField;
|
||||
}) {
|
||||
const [teamName, setTeamName] = useState(props.team.name);
|
||||
const maxTeamNameLength = 32;
|
||||
|
||||
const form = useForm<{ name: string }>({
|
||||
values: { name: props.team.name },
|
||||
resolver: zodResolver(teamNameFormSchema),
|
||||
});
|
||||
const updateTeamMutation = useMutation({
|
||||
mutationFn: (name: string) => props.updateTeamField({ name }),
|
||||
});
|
||||
|
||||
function handleSave() {
|
||||
const promises = updateTeamMutation.mutateAsync(teamName);
|
||||
toast.promise(promises, {
|
||||
success: "Team name updated successfully",
|
||||
error: "Failed to update team name",
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsCard
|
||||
header={{
|
||||
title: "Team Name",
|
||||
description: "This is your team's name on thirdweb",
|
||||
}}
|
||||
bottomText={`Please use ${maxTeamNameLength} characters at maximum.`}
|
||||
saveButton={{
|
||||
onClick: handleSave,
|
||||
disabled: teamName.length === 0,
|
||||
isPending: updateTeamMutation.isPending,
|
||||
}}
|
||||
errorText={undefined}
|
||||
noPermissionText={undefined} // TODO
|
||||
<form
|
||||
onSubmit={form.handleSubmit((values) => {
|
||||
const promise = updateTeamMutation.mutateAsync(values.name);
|
||||
toast.promise(promise, {
|
||||
success: "Team name updated successfully",
|
||||
error: "Failed to update team name",
|
||||
});
|
||||
})}
|
||||
>
|
||||
<Input
|
||||
value={teamName}
|
||||
maxLength={maxTeamNameLength}
|
||||
onChange={(e) => {
|
||||
setTeamName(e.target.value);
|
||||
<SettingsCard
|
||||
header={{
|
||||
title: "Team Name",
|
||||
description: "This is your team's name on thirdweb",
|
||||
}}
|
||||
className="md:w-[450px]"
|
||||
/>
|
||||
</SettingsCard>
|
||||
bottomText={`Please use ${maxTeamNameLength} characters at maximum.`}
|
||||
saveButton={{
|
||||
type: "submit",
|
||||
disabled: !form.formState.isDirty,
|
||||
isPending: updateTeamMutation.isPending,
|
||||
}}
|
||||
errorText={form.formState.errors.name?.message}
|
||||
noPermissionText={undefined}
|
||||
>
|
||||
<Input
|
||||
{...form.register("name")}
|
||||
maxLength={maxTeamNameLength}
|
||||
className="md:w-[450px]"
|
||||
/>
|
||||
</SettingsCard>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
const teamSlugFormSchema = z.object({
|
||||
slug: teamSlugSchema,
|
||||
});
|
||||
|
||||
function TeamSlugFormControl(props: {
|
||||
team: Team;
|
||||
updateTeamField: (team: Partial<Team>) => Promise<void>;
|
||||
}) {
|
||||
const [teamSlug, setTeamSlug] = useState(props.team.slug);
|
||||
const maxTeamURLLength = 48;
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>();
|
||||
|
||||
const form = useForm<{ slug: string }>({
|
||||
defaultValues: { slug: props.team.slug },
|
||||
resolver: zodResolver(teamSlugFormSchema),
|
||||
});
|
||||
const updateTeamMutation = useMutation({
|
||||
mutationFn: (slug: string) => props.updateTeamField({ slug: slug }),
|
||||
mutationFn: (slug: string) => props.updateTeamField({ slug }),
|
||||
});
|
||||
|
||||
function handleSave() {
|
||||
const promises = updateTeamMutation.mutateAsync(teamSlug);
|
||||
toast.promise(promises, {
|
||||
success: "Team URL updated successfully",
|
||||
error: "Failed to update team URL",
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsCard
|
||||
header={{
|
||||
title: "Team URL",
|
||||
description:
|
||||
"This is your team's URL namespace on thirdweb. All your team's projects and settings can be accessed using this URL",
|
||||
}}
|
||||
bottomText={`Please use ${maxTeamURLLength} characters at maximum.`}
|
||||
errorText={errorMessage}
|
||||
saveButton={{
|
||||
onClick: handleSave,
|
||||
disabled: errorMessage !== undefined,
|
||||
isPending: updateTeamMutation.isPending,
|
||||
}}
|
||||
noPermissionText={undefined} // TODO
|
||||
<form
|
||||
onSubmit={form.handleSubmit((values) => {
|
||||
const promise = updateTeamMutation.mutateAsync(values.slug);
|
||||
toast.promise(promise, {
|
||||
success: "Team URL updated successfully",
|
||||
error: "Failed to update team URL",
|
||||
});
|
||||
})}
|
||||
>
|
||||
<div className="relative flex rounded-lg border border-border md:w-[450px]">
|
||||
<div className="flex items-center self-stretch rounded-l-lg border-border border-r bg-card px-3 font-mono text-muted-foreground/80 text-sm">
|
||||
thirdweb.com/team/
|
||||
<SettingsCard
|
||||
header={{
|
||||
title: "Team URL",
|
||||
description:
|
||||
"This is your team's URL namespace on thirdweb. All your team's projects and settings can be accessed using this URL",
|
||||
}}
|
||||
bottomText={`Please use ${maxTeamSlugLength} characters at maximum.`}
|
||||
errorText={form.formState.errors.slug?.message}
|
||||
saveButton={{
|
||||
type: "submit",
|
||||
disabled: !form.formState.isDirty,
|
||||
isPending: updateTeamMutation.isPending,
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
>
|
||||
<div className="relative flex rounded-lg border border-border md:w-[450px]">
|
||||
<div className="flex items-center self-stretch rounded-l-lg border-border border-r bg-card px-3 font-mono text-muted-foreground/80 text-sm">
|
||||
thirdweb.com/team/
|
||||
</div>
|
||||
<Input
|
||||
{...form.register("slug")}
|
||||
maxLength={maxTeamSlugLength}
|
||||
className="truncate border-0 font-mono"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
value={teamSlug}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.slice(0, maxTeamURLLength);
|
||||
setTeamSlug(value);
|
||||
if (value.trim().length === 0) {
|
||||
setErrorMessage("Team URL can not be empty");
|
||||
} else if (teamSlugRegex.test(value)) {
|
||||
setErrorMessage(
|
||||
"Invalid Team URL. Only letters, numbers and hyphens are allowed",
|
||||
);
|
||||
} else {
|
||||
setErrorMessage(undefined);
|
||||
}
|
||||
}}
|
||||
className="truncate border-0 font-mono"
|
||||
/>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
</SettingsCard>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -186,8 +192,8 @@ function TeamAvatarFormControl(props: {
|
||||
});
|
||||
|
||||
function handleSave() {
|
||||
const promises = updateTeamAvatarMutation.mutateAsync(teamAvatar);
|
||||
toast.promise(promises, {
|
||||
const promise = updateTeamAvatarMutation.mutateAsync(teamAvatar);
|
||||
toast.promise(promise, {
|
||||
success: "Team avatar updated successfully",
|
||||
error: "Failed to update team avatar",
|
||||
});
|
||||
@@ -263,8 +269,8 @@ export function LeaveTeamCard(props: {
|
||||
});
|
||||
|
||||
function handleLeave() {
|
||||
const promises = leaveTeam.mutateAsync();
|
||||
toast.promise(promises, {
|
||||
const promise = leaveTeam.mutateAsync();
|
||||
toast.promise(promise, {
|
||||
success: "Left team successfully",
|
||||
error: "Failed to leave team",
|
||||
});
|
||||
@@ -308,8 +314,8 @@ export function DeleteTeamCard(props: {
|
||||
});
|
||||
|
||||
function handleDelete() {
|
||||
const promises = deleteTeam.mutateAsync();
|
||||
toast.promise(promises, {
|
||||
const promise = deleteTeam.mutateAsync();
|
||||
toast.promise(promise, {
|
||||
success: "Team deleted successfully",
|
||||
error: "Failed to delete team",
|
||||
});
|
||||
|
||||
@@ -1 +1,40 @@
|
||||
export const teamSlugRegex = /[^a-zA-Z0-9-]/;
|
||||
import { z } from "zod";
|
||||
|
||||
const teamSlugRegex = /^[a-z0-9]+(?:[-][a-z0-9]+)*$/;
|
||||
const teamNameRegex = /^[A-Za-z0-9]+(?:[ -][A-Za-z0-9]+)*$/;
|
||||
|
||||
export const maxTeamSlugLength = 48;
|
||||
const minTeamSlugLength = 3;
|
||||
|
||||
export const teamSlugSchema = z
|
||||
.string()
|
||||
.min(minTeamSlugLength, {
|
||||
message: `Team slug must be at least ${minTeamSlugLength} characters`,
|
||||
})
|
||||
.max(maxTeamSlugLength, {
|
||||
message: `Team slug must be at most ${maxTeamSlugLength} characters`,
|
||||
})
|
||||
.refine((slug) => !slug.includes(" "), {
|
||||
message: "Team URL cannot contain spaces",
|
||||
})
|
||||
.refine((slug) => teamSlugRegex.test(slug), {
|
||||
message: "Team URL can only contain lowercase letters, numbers and hyphens",
|
||||
});
|
||||
|
||||
export const maxTeamNameLength = 32;
|
||||
const minTeamNameLength = 3;
|
||||
|
||||
export const teamNameSchema = z
|
||||
.string()
|
||||
.min(minTeamNameLength, {
|
||||
message: `Team name must be at least ${minTeamNameLength} characters`,
|
||||
})
|
||||
.max(maxTeamNameLength, {
|
||||
message: `Team name must be at most ${maxTeamNameLength} characters`,
|
||||
})
|
||||
.refine((name) => name.trim() === name, {
|
||||
message: "Team name cannot contain leading or trailing spaces",
|
||||
})
|
||||
.refine((name) => teamNameRegex.test(name), {
|
||||
message: "Team name can only contain letters, numbers, spaces and hyphens",
|
||||
});
|
||||
|
||||
@@ -13,10 +13,10 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRight,
|
||||
CreditCard,
|
||||
Download,
|
||||
Receipt,
|
||||
ChevronRightIcon,
|
||||
CreditCardIcon,
|
||||
DownloadIcon,
|
||||
ReceiptIcon,
|
||||
} from "lucide-react";
|
||||
import { useQueryState } from "nuqs";
|
||||
import { useTransition } from "react";
|
||||
@@ -78,14 +78,14 @@ export function BillingHistory(props: {
|
||||
if (props.status === "open") {
|
||||
return (
|
||||
<div className="py-6 text-center">
|
||||
<Receipt className="mx-auto h-12 w-12 text-muted-foreground" />
|
||||
<ReceiptIcon className="mx-auto h-12 w-12 text-muted-foreground" />
|
||||
<h3 className="mt-2 font-medium text-lg">No open invoices</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="py-6 text-center">
|
||||
<Receipt className="mx-auto h-12 w-12 text-muted-foreground" />
|
||||
<ReceiptIcon className="mx-auto h-12 w-12 text-muted-foreground" />
|
||||
<h3 className="mt-2 font-medium text-lg">No billing history</h3>
|
||||
<p className="mt-1 text-muted-foreground text-sm">
|
||||
Your invoice history will appear here once you have made payments.
|
||||
@@ -130,7 +130,7 @@ export function BillingHistory(props: {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<CreditCard className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||
<CreditCardIcon className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||
Pay Now
|
||||
</a>
|
||||
</Button>
|
||||
@@ -143,7 +143,7 @@ export function BillingHistory(props: {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Download className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||
<DownloadIcon className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||
PDF
|
||||
</a>
|
||||
</Button>
|
||||
@@ -190,7 +190,7 @@ export function BillingHistory(props: {
|
||||
{isLoading && props.hasMore ? (
|
||||
<div className="ml-2 h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
|
||||
) : (
|
||||
<ChevronRight className="ml-2 h-4 w-4" />
|
||||
<ChevronRightIcon className="ml-2 h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
PlusIcon,
|
||||
SearchIcon,
|
||||
Trash2Icon,
|
||||
UserPlus,
|
||||
UserPlusIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
@@ -183,7 +183,7 @@ export function InviteSection(props: {
|
||||
{sendInvites.isPending ? (
|
||||
<Spinner className="size-4" />
|
||||
) : (
|
||||
<UserPlus className="size-4" />
|
||||
<UserPlusIcon className="size-4" />
|
||||
)}
|
||||
{form.watch("invites").length > 1
|
||||
? "Send Invites"
|
||||
|
||||
@@ -47,7 +47,9 @@ export function AccountAbstractionLayout(props: {
|
||||
</TrackedUnderlineLink>
|
||||
</p>
|
||||
</div>
|
||||
{props.hasSmartWalletsWithoutBilling && <SmartWalletsBillingAlert />}
|
||||
{props.hasSmartWalletsWithoutBilling && (
|
||||
<SmartWalletsBillingAlert teamSlug={props.teamSlug} />
|
||||
)}
|
||||
<GasCreditAlert
|
||||
teamSlug={props.teamSlug}
|
||||
projectId={props.projectId}
|
||||
|
||||
@@ -27,6 +27,7 @@ export default async function Page(props: {
|
||||
|
||||
let fees = await getFees({
|
||||
clientId: project.publishableKey,
|
||||
teamId: team.id,
|
||||
}).catch(() => {
|
||||
return {
|
||||
feeRecipient: "",
|
||||
|
||||
@@ -358,6 +358,8 @@ export function ProjectGeneralSettingsPageUI(props: {
|
||||
rotateSecretKey={props.rotateSecretKey}
|
||||
/>
|
||||
|
||||
<ProjectIdCard project={project} />
|
||||
|
||||
<AllowedDomainsSetting
|
||||
form={form}
|
||||
isUpdatingProject={updateProject.isPending}
|
||||
@@ -434,6 +436,31 @@ function ProjectNameSetting(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectIdCard(props: {
|
||||
project: Project;
|
||||
}) {
|
||||
return (
|
||||
<SettingsCard
|
||||
header={{
|
||||
title: "Project ID",
|
||||
description: "This is your project's ID on thirdweb",
|
||||
}}
|
||||
bottomText="Used when interacting with the thirdweb API"
|
||||
noPermissionText={undefined}
|
||||
errorText={undefined}
|
||||
>
|
||||
<CopyTextButton
|
||||
textToCopy={props.project.id}
|
||||
textToShow={props.project.id}
|
||||
variant="outline"
|
||||
className="w-full justify-between truncate bg-background px-3 py-2 font-mono text-muted-foreground lg:w-[450px]"
|
||||
tooltip="Copy Project ID"
|
||||
copyIconPosition="right"
|
||||
/>
|
||||
</SettingsCard>
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectImageSetting(props: {
|
||||
updateProjectImage: (file: File | undefined) => Promise<void>;
|
||||
avatar: string | null;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getTeamBySlug } from "@/api/team";
|
||||
import { OpCreditsGrantedModalWrapperServer } from "components/onboarding/OpCreditsGrantedModalWrapperServer";
|
||||
import { PosthogIdentifierServer } from "components/wallets/PosthogIdentifierServer";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
@@ -42,9 +41,6 @@ export default async function RootTeamLayout(props: {
|
||||
|
||||
<SaveLastVisitedTeamPage teamId={team.id} />
|
||||
|
||||
<Suspense fallback={null}>
|
||||
<OpCreditsGrantedModalWrapperServer />
|
||||
</Suspense>
|
||||
<Suspense fallback={null}>
|
||||
<EnsureValidConnectedWalletLoginServer />
|
||||
</Suspense>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { getDefaultTeam } from "@/api/team";
|
||||
import { getLastVisitedTeamOrDefaultTeam } from "@/api/team";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
|
||||
export default async function TeamRootPage() {
|
||||
const firstTeam = await getDefaultTeam();
|
||||
if (!firstTeam) {
|
||||
const team = await getLastVisitedTeamOrDefaultTeam();
|
||||
if (!team) {
|
||||
notFound();
|
||||
}
|
||||
redirect(`/team/${firstTeam.slug}`);
|
||||
redirect(`/team/${team.slug}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { type Team, getTeams } from "@/api/team";
|
||||
import { GradientAvatar } from "@/components/blocks/Avatars/GradientAvatar";
|
||||
import { AppFooter } from "@/components/blocks/app-footer";
|
||||
import { DotsBackgroundPattern } from "@/components/ui/background-patterns";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { ChevronRightIcon, UsersIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getAuthToken } from "../../../api/lib/getAuthToken";
|
||||
import { TeamPlanBadge } from "../../../components/TeamPlanBadge";
|
||||
import { TeamHeader } from "../../components/TeamHeader/team-header";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
paths?: string[];
|
||||
}>;
|
||||
searchParams: Promise<Record<string, string | undefined | string[]>>;
|
||||
}) {
|
||||
const [params, searchParams, teams, authToken] = await Promise.all([
|
||||
props.params,
|
||||
props.searchParams,
|
||||
getTeams(),
|
||||
getAuthToken(),
|
||||
]);
|
||||
|
||||
if (!teams || !authToken) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const searchParamsString = Object.entries(searchParams)
|
||||
.map(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((v) => `${key}=${encodeURIComponent(String(v))}`)
|
||||
.join("&");
|
||||
}
|
||||
return `${key}=${encodeURIComponent(String(value))}`;
|
||||
})
|
||||
.join("&");
|
||||
|
||||
// if there is a single team, redirect to the team page directly
|
||||
if (teams.length === 1 && teams[0]) {
|
||||
redirect(
|
||||
createTeamLink({
|
||||
team: teams[0],
|
||||
paths: params.paths,
|
||||
searchParams: searchParamsString,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const client = getThirdwebClient({
|
||||
jwt: authToken,
|
||||
teamId: undefined,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-dvh flex-col ">
|
||||
<div className="border-b bg-card">
|
||||
<TeamHeader />
|
||||
</div>
|
||||
|
||||
<div className="relative flex grow flex-col overflow-hidden">
|
||||
<DotsBackgroundPattern />
|
||||
<div className="container z-10 flex grow flex-col items-center justify-center py-20">
|
||||
<div className="w-full max-w-lg rounded-xl border bg-card shadow-2xl">
|
||||
<div className="flex flex-col border-b p-4 lg:p-6">
|
||||
<div className="mb-2 self-start rounded-full border p-2">
|
||||
<UsersIcon className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
<h1 className="mb-0.5 font-semibold text-xl tracking-tight">
|
||||
Select a team
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
You are currently a member of multiple teams
|
||||
<br />
|
||||
Select a team to view this page
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col [&>*:not(:last-child)]:border-b">
|
||||
{teams.map((team) => {
|
||||
return (
|
||||
<div
|
||||
key={team.id}
|
||||
className="group relative flex items-center gap-3 px-4 py-4 hover:bg-accent/50 lg:px-6"
|
||||
>
|
||||
<GradientAvatar
|
||||
src={team.image || ""}
|
||||
id={team.id}
|
||||
className="size-8 rounded-full border"
|
||||
client={client}
|
||||
/>
|
||||
<Link
|
||||
href={createTeamLink({
|
||||
team,
|
||||
paths: params.paths,
|
||||
searchParams: searchParamsString,
|
||||
})}
|
||||
className="before:absolute before:inset-0"
|
||||
>
|
||||
{team.name}
|
||||
</Link>
|
||||
<TeamPlanBadge plan={team.billingPlan} />
|
||||
<ChevronRightIcon className="ml-auto size-4 text-muted-foreground group-hover:text-foreground" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AppFooter className="relative z-10" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function createTeamLink(params: {
|
||||
team: Team;
|
||||
paths: string[] | undefined;
|
||||
searchParams: string | undefined;
|
||||
}) {
|
||||
return `/team/${params.team.slug}/${(params.paths || [])?.join("/") || ""}${params.searchParams ? `?${params.searchParams}` : ""}`;
|
||||
}
|
||||