Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/better-owls-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"thirdweb": patch
---

- Add support for blob urls in `MediaRenderer` component
- Fix `className` prop not set in loading state of `MediaRenderer` component
6 changes: 4 additions & 2 deletions apps/dashboard/src/@/components/blocks/Img.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type imgElementProps = React.DetailedHTMLProps<
skeleton?: React.ReactNode;
fallback?: React.ReactNode;
src: string | undefined;
containerClassName?: string;
};

export function Img(props: imgElementProps) {
Expand All @@ -23,7 +24,8 @@ export function Img(props: imgElementProps) {
: props.src === ""
? "fallback"
: _status;
const { className, fallback, skeleton, ...restProps } = props;
const { className, fallback, skeleton, containerClassName, ...restProps } =
props;
const defaultSkeleton = <div className="animate-pulse bg-accent" />;
const defaultFallback = <div className="bg-accent" />;
const imgRef = useRef<HTMLImageElement>(null);
Expand All @@ -47,7 +49,7 @@ export function Img(props: imgElementProps) {
}, []);

return (
<div className="relative shrink-0">
<div className={cn("relative shrink-0", containerClassName)}>
<img
{...restProps}
// avoid setting empty src string to prevent request to the entire page
Expand Down
13 changes: 12 additions & 1 deletion apps/dashboard/src/@/components/blocks/TokenSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,17 @@ export function TokenSelector(props: {
? `${props.selectedToken.chainId}:${props.selectedToken.address}`
: undefined;

// if selected value is not in options, add it
if (
selectedValue &&
!options.find((option) => option.value === selectedValue)
) {
options.push({
label: props.selectedToken?.address || "Unknown",
value: selectedValue,
});
}

return (
<SelectWithSearch
searchPlaceholder="Search by name or symbol"
Expand All @@ -175,7 +186,7 @@ export function TokenSelector(props: {
showCheck={props.showCheck}
placeholder={
tokensQuery.isPending
? "Loading Tokens..."
? "Loading Tokens"
: props.placeholder || "Select Token"
}
overrideSearchFn={searchFn}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { Meta, StoryObj } from "@storybook/react";
import { DropZone } from "./drop-zone";

const meta = {
title: "blocks/DropZone",
component: DropZone,
decorators: [
(Story) => (
<div className="container max-w-6xl py-10">
<Story />
</div>
),
],
} satisfies Meta<typeof DropZone>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
isError: false,
onDrop: () => {},
title: "This is a title",
description: "This is a description for drop zone",
accept: undefined,
resetButton: undefined,
},
};

export const ErrorState: Story = {
args: {
isError: true,
onDrop: () => {},
title: "this is title",
description: "This is a description",
accept: undefined,
resetButton: undefined,
},
};

export const ErrorStateWithResetButton: Story = {
args: {
isError: true,
onDrop: () => {},
title: "this is title",
description: "This is a description",
accept: undefined,
resetButton: {
label: "Remove Files",
onClick: () => {},
},
},
};
81 changes: 81 additions & 0 deletions apps/dashboard/src/@/components/blocks/drop-zone/drop-zone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { UploadIcon, XIcon } from "lucide-react";
import { useDropzone } from "react-dropzone";

export function DropZone(props: {
isError: boolean;
onDrop: (acceptedFiles: File[]) => void;
title: string;
description: string;
resetButton:
| {
label: string;
onClick: () => void;
}
| undefined;
className?: string;
accept: string | undefined;
}) {
const { getRootProps, getInputProps } = useDropzone({
onDrop: props.onDrop,
});

const { resetButton } = props;

return (
<div
className={cn(
"flex cursor-pointer items-center justify-center rounded-md border border-dashed bg-card py-10 hover:border-active-border",
props.isError &&
"border-red-500 bg-red-200/30 text-red-500 hover:border-red-600 dark:border-red-900 dark:bg-red-900/30 dark:hover:border-red-800",
props.className,
)}
{...getRootProps()}
>
<input {...getInputProps()} accept={props.accept} />
<div className="flex flex-col items-center justify-center gap-3">
{!props.isError && (
<div className="flex flex-col items-center">
<div className="mb-3 flex size-11 items-center justify-center rounded-full border bg-card">
<UploadIcon className="size-5" />
</div>
<h2 className="mb-0.5 text-center font-medium text-lg">
{props.title}
</h2>
<p className="text-center font-medium text-muted-foreground text-sm">
{props.description}
</p>
</div>
)}

{props.isError && (
<div className="flex flex-col items-center">
<div className="mb-3 flex size-11 items-center justify-center rounded-full border border-red-500 bg-red-200/50 text-red-500 dark:border-red-900 dark:bg-red-900/30 dark:text-foreground">
<XIcon className="size-5" />
</div>
<h2 className="mb-0.5 text-center font-medium text-foreground text-lg">
{props.title}
</h2>
<p className="text-balance text-center text-sm">
{props.description}
</p>

{resetButton && (
<Button
className="relative z-50 mt-4"
size="sm"
onClick={(e) => {
e.stopPropagation();
resetButton.onClick();
}}
>
{resetButton.label}
</Button>
)}
</div>
)}
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,45 +16,34 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

export const AllStates: Story = {
args: {
onRetry: () => {},
steps: [
{
status: { type: "completed" },
label: "Connect Wallet",
execute: async () => {
await sleep(1000);
},
id: "connect-wallet",
},
{
status: { type: "pending" },
label: "Sign Message",
execute: async () => {
await sleep(1000);
},
id: "sign-message",
},
{
status: { type: "error", message: "This is an error message" },
label: "Approve Transaction",
execute: async () => {
await sleep(1000);
},
id: "approve-transaction",
},
{
status: { type: "idle" },
label: "Confirm Transaction",
execute: async () => {
await sleep(1000);
},
id: "confirm-transaction",
},
{
status: { type: "idle" },
label: "Finalize",
execute: async () => {
await sleep(1000);
},
id: "finalize",
},
],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,23 @@ import {
import { DynamicHeight } from "../../ui/DynamicHeight";
import { Spinner } from "../../ui/Spinner/Spinner";

export type MultiStepState = {
export type MultiStepState<T extends string> = {
id: T;
status:
| {
type: "idle" | "pending" | "completed";
}
| {
type: "error";
message: string | React.ReactNode;
message: React.ReactNode;
};
label: string;
execute: () => Promise<void>;
description?: string;
};

export function MultiStepStatus(props: {
steps: MultiStepState[];
export function MultiStepStatus<T extends string>(props: {
steps: MultiStepState<T>[];
onRetry: (step: MultiStepState<T>) => void;
}) {
return (
<DynamicHeight>
Expand Down Expand Up @@ -55,6 +57,15 @@ export function MultiStepStatus(props: {
{step.label}
</p>

{/* show description when this step is active */}
{(step.status.type === "pending" ||
step.status.type === "error") &&
step.description && (
<p className="text-muted-foreground text-sm">
{step.description}
</p>
)}

{step.status.type === "error" && (
<div className="mt-1 space-y-2">
<p className="mb-1 text-red-500 text-sm">
Expand All @@ -64,7 +75,7 @@ export function MultiStepStatus(props: {
variant="destructive"
size="sm"
className="gap-2"
onClick={() => step.execute()}
onClick={() => props.onRetry(step)}
>
<RefreshCwIcon className="size-4" />
Retry
Expand Down
2 changes: 2 additions & 0 deletions apps/dashboard/src/@/components/ui/decimal-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export function DecimalInput(props: {
id?: string;
className?: string;
placeholder?: string;
disabled?: boolean;
}) {
return (
<Input
Expand All @@ -15,6 +16,7 @@ export function DecimalInput(props: {
className={props.className}
inputMode="decimal"
placeholder={props.placeholder}
disabled={props.disabled}
onChange={(e) => {
const number = Number(e.target.value);
// ignore if string becomes invalid number
Expand Down
4 changes: 4 additions & 0 deletions apps/dashboard/src/@/lib/file-to-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export function fileToBlobUrl(file: File) {
const blob = new Blob([file], { type: file.type });
return URL.createObjectURL(blob);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { getAuthToken } from "@app/api/lib/getAuthToken";
import { ArrowUpRightIcon } from "lucide-react";
import type { Metadata } from "next";
import { headers } from "next/headers";
import { getAuthToken } from "../../../api/lib/getAuthToken";
import { SearchInput } from "./components/client/search";
import { QueryType } from "./components/client/type";
import { RouteListView } from "./components/client/view";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
useSwitchActiveWalletChain,
useWalletBalance,
} from "thirdweb/react";
import { parseError } from "utils/errorParser";
import { z } from "zod";

function formatTime(seconds: number) {
Expand Down Expand Up @@ -234,7 +235,12 @@ export function FaucetButton({
const claimPromise = claimMutation.mutateAsync(values.turnstileToken);
toast.promise(claimPromise, {
success: `${amount} ${chain.nativeCurrency.symbol} sent successfully`,
error: `Failed to claim ${amount} ${chain.nativeCurrency.symbol}`,
error: (err) => {
return {
message: `Failed to claim ${amount} ${chain.nativeCurrency.symbol}`,
description: parseError(err),
};
},
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import {
getAuthToken,
getAuthTokenWalletAddress,
} from "@app/api/lib/getAuthToken";
import { ChevronDownIcon, TicketCheckIcon } from "lucide-react";
import type { Metadata } from "next";
import Link from "next/link";
import { redirect } from "next/navigation";
import { mapV4ChainToV5Chain } from "../../../../../../contexts/map-chains";
import { NebulaChatButton } from "../../../../../nebula-app/(app)/components/FloatingChat/FloatingChat";
import {
getAuthToken,
getAuthTokenWalletAddress,
} from "../../../../api/lib/getAuthToken";
import { TeamHeader } from "../../../../team/components/TeamHeader/team-header";
import { StarButton } from "../../components/client/star-button";
import { getChain, getChainMetadata } from "../../utils";
Expand Down
Loading
Loading