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
1 change: 1 addition & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"react-day-picker": "^8.10.1",
"react-dom": "18.3.1",
"react-dropzone": "^14.2.9",
"react-error-boundary": "^4.1.2",
"react-hook-form": "7.52.0",
"react-intersection-observer": "^9.10.3",
"react-markdown": "^9.0.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
import { useMemo } from "react";

export function UnexpectedValueErrorMessage(props: {
value: unknown;
title: string;
description: string;
className?: string;
}) {
const stringifiedValue = useMemo(() => {
try {
return JSON.stringify(props.value, null, 2);
} catch {
return undefined;
}
}, [props.value]);

return (
<div className={props.className}>
<p className="text-base text-destructive-text"> {props.title} </p>
<p className="text-foreground text-sm">{props.description}</p>
{stringifiedValue && (
<div className="mt-3">
<p className="mb-0.5 text-muted-foreground text-sm">Value Received</p>
<ScrollShadow className="rounded-lg bg-muted/50">
<code className="block whitespace-pre p-4 font-mono text-xs">
{stringifiedValue}
</code>
</ScrollShadow>

<CopyTextButton
copyIconPosition="left"
textToShow="Copy value"
textToCopy={stringifiedValue}
tooltip="Copy value"
className="-translate-x-2 mt-1 text-muted-foreground"
variant="ghost"
/>
</div>
)}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { UnexpectedValueErrorMessage } from "@/components/blocks/error-fallbacks/unexpect-value-error-message";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Spinner } from "@/components/ui/Spinner/Spinner";
Expand Down Expand Up @@ -53,6 +54,8 @@ interface TokenIdPageProps {
isErc721: boolean;
}

// TODO: verify the entire nft object with zod schema and display an error message

export const TokenIdPage: React.FC<TokenIdPageProps> = ({
contract,
tokenId,
Expand Down Expand Up @@ -132,15 +135,15 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
height={isMobile ? "100%" : "300px"}
/>
</Card>

<Flex flexDir="column" gap={6} w="full" px={2}>
<Flex flexDir="column" gap={2}>
<Heading size="title.lg">{nft.metadata.name}</Heading>
<Flex flexDir="column" gap={1.5}>
<NFTName value={nft.metadata.name} />
{nft.metadata?.description && (
<Text size="label.md" noOfLines={50} whiteSpace="pre-wrap">
{nft.metadata.description}
</Text>
<NFTDescription value={nft.metadata.description} />
)}
</Flex>

<Flex flexDir="column" gap={{ base: 0, md: 4 }}>
<Box
w="full"
Expand Down Expand Up @@ -338,3 +341,39 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
</Flex>
);
};

function NFTName(props: {
value: unknown;
}) {
if (typeof props.value === "string") {
return (
<h2 className="font-semibold text-lg tracking-tight"> {props.value}</h2>
);
}

return (
<UnexpectedValueErrorMessage
title="Invalid Name"
description="Name is not a string"
value={props.value}
className="mb-3 rounded-lg border border-border p-4"
/>
);
}

function NFTDescription(props: {
value: unknown;
}) {
if (typeof props.value === "string") {
return <p className="text-muted-foreground"> {props.value}</p>;
}

return (
<UnexpectedValueErrorMessage
title="Invalid Description"
description="Description is not a string"
value={props.value}
className="mb-3 rounded-lg border border-border p-4"
/>
);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"use client";

import { UnexpectedValueErrorMessage } from "@/components/blocks/error-fallbacks/unexpect-value-error-message";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { cn } from "@/lib/utils";
import {
Flex,
IconButton,
Expand All @@ -16,6 +19,7 @@ import {
Thead,
Tr,
} from "@chakra-ui/react";
import * as Sentry from "@sentry/nextjs";
import { MediaCell } from "components/contract-pages/table/table-columns/cells/media-cell";
import { useChainSlug } from "hooks/chains/chainSlug";
import {
Expand All @@ -26,6 +30,7 @@ import {
ChevronRightIcon,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { ErrorBoundary, type FallbackProps } from "react-error-boundary";
import {
type CellProps,
type Column,
Expand Down Expand Up @@ -72,24 +77,51 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
{
Header: "Name",
accessor: (row) => row.metadata.name,
Cell: (cell: CellProps<NFT, string>) => (
<Text noOfLines={1} size="label.md">
{cell.value}
</Text>
),
Cell: (cell: CellProps<NFT, string>) => {
if (typeof cell.value !== "string") {
return (
<UnexpectedValueErrorMessage
title="Invalid Name"
description="Name is not a string"
value={cell.value}
className="w-[300px] py-3"
/>
);
}

return (
<p className="line-clamp-1 text-muted-foreground text-sm">
{cell.value}
</p>
);
},
},
{
Header: "Description",
accessor: (row) => row.metadata.description,
Cell: (cell: CellProps<NFT, string>) => (
<Text
noOfLines={4}
size="body.md"
fontStyle={!cell.value ? "italic" : "normal"}
>
{cell.value || "No description"}
</Text>
),
Cell: (cell: CellProps<NFT, string>) => {
if (typeof cell.value !== "string") {
return (
<UnexpectedValueErrorMessage
title="Invalid description"
description="Description is not a string"
value={cell.value}
className="w-[300px] py-3"
/>
);
}

return (
<p
className={cn(
"line-clamp-4 max-w-[200px] whitespace-normal text-muted-foreground text-sm",
{ italic: !cell.value },
)}
>
{cell.value || "No description"}
</p>
);
},
},
];
if (isErc721) {
Expand Down Expand Up @@ -280,18 +312,22 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
key={rowIndex}
>
{row.cells.map((cell, cellIndex) => (
<Td
{...cell.getCellProps()}
borderBottomWidth="inherit"
borderColor="borderColor"
maxW="sm"
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
key={cellIndex}
>
{cell.render("Cell")}
</Td>
))}
{row.cells.map((cell, cellIndex) => {
return (
<Td
{...cell.getCellProps()}
borderBottomWidth="inherit"
borderColor="borderColor"
maxW="sm"
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
key={cellIndex}
>
<ErrorBoundary FallbackComponent={NFTCellErrorBoundary}>
{cell.render("Cell")}
</ErrorBoundary>
</Td>
);
})}
<Td borderBottomWidth="inherit" borderColor="borderColor">
{!failedToLoad && <ArrowRightIcon className="size-4" />}
</Td>
Expand Down Expand Up @@ -374,3 +410,25 @@ export const NFTGetAllTable: React.FC<ContractOverviewNFTGetAllProps> = ({
</Flex>
);
};

function NFTCellErrorBoundary(errorProps: FallbackProps) {
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
Sentry.withScope((scope) => {
scope.setTag("component-crashed", "true");
scope.setTag("component-crashed-boundary", "NFTCellErrorBoundary");
scope.setLevel("fatal");
Sentry.captureException(errorProps.error);
});
}, [errorProps.error]);

return (
<CopyTextButton
textToShow={errorProps.error.message}
textToCopy={errorProps.error.message}
copyIconPosition="left"
tooltip={errorProps.error.message}
className="max-w-[250px] truncate text-destructive-text"
/>
);
}
27 changes: 20 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading