Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9c44b45
Add transaction activity logs with expandable timeline view
cursoragent Jul 4, 2025
1b2b3fa
Checkpoint before follow-up message
cursoragent Jul 4, 2025
6858fba
Checkpoint before follow-up message
cursoragent Jul 4, 2025
d5df1ec
Checkpoint before follow-up message
cursoragent Jul 4, 2025
933aa1d
Refactor server client creation and add fallback for missing secret key
cursoragent Jul 4, 2025
a29b8c7
feat: add activity log section to transaction details page
cursoragent Jul 5, 2025
707076d
feat: improve activity log visual indicators and timestamp handling
cursoragent Jul 5, 2025
29310fb
fix: use same timestamp pattern as Timing Information card
cursoragent Jul 5, 2025
ad64fb9
feat: use createdAt instead of timestamp for activity log time displays
cursoragent Jul 5, 2025
9a42ff8
fix: correct event type matching for red dot color
cursoragent Jul 5, 2025
6f18324
feat: sort activity logs chronologically (oldest first)
cursoragent Jul 5, 2025
e82de0c
Refactor activity log entry UI with improved event type styling
cursoragent Jul 5, 2025
17dc805
Checkpoint before follow-up message
cursoragent Jul 5, 2025
c6c969f
Fix indentation in transaction details error handling logic
cursoragent Jul 5, 2025
0ece641
Refactor event type colors to use Badge component with variants
cursoragent Jul 5, 2025
ecc5479
Refactor activity log rendering with explicit sorting and rendering l…
cursoragent Jul 5, 2025
ccc3837
Refactor activity log sorting to use insertion sort algorithm
cursoragent Jul 5, 2025
3a44bd7
Checkpoint before follow-up message
cursoragent Jul 5, 2025
91a8db8
Remove timestamp from activity log entry item
cursoragent Jul 5, 2025
b106e3d
revert robots.txt change
joaquim-verges Jul 5, 2025
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
2 changes: 1 addition & 1 deletion apps/dashboard/public/robots.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# *
User-agent: *
Allow: /
Disallow: /

# Host
Host: https://thirdweb.com
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ImageResponse } from "next/og";
import { useId } from "react";
import { download } from "thirdweb/storage";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { fetchChain } from "@/utils/fetchChain";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add server-only import for server component.

This is a server-side component that should include the import "server-only"; directive to ensure it never runs in the browser.

Add this import at the top of the file:

+import "server-only";
 import { ImageResponse } from "next/og";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { ImageResponse } from "next/og";
import { useId } from "react";
import { download } from "thirdweb/storage";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { fetchChain } from "@/utils/fetchChain";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import "server-only";
import { ImageResponse } from "next/og";
import { useId } from "react";
import { download } from "thirdweb/storage";
import { fetchChain } from "@/utils/fetchChain";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/(chainPage)/opengraph-image.tsx
lines 1 to 7, add the import statement 'import "server-only";' at the very top
of the file to mark this as a server-only component and prevent it from running
in the browser.

// Route segment config
export const runtime = "edge";
Expand Down Expand Up @@ -81,16 +82,29 @@ export default async function Image({
fetch(new URL("og-lib/fonts/inter/700.ttf", import.meta.url)).then((res) =>
res.arrayBuffer(),
),
// download the chain icon if there is one
chain.icon?.url && hasWorkingChainIcon
? download({
client: serverThirdwebClient,
uri: chain.icon.url,
}).then((res) => res.arrayBuffer())
// download the chain icon if there is one and secret key is available
chain.icon?.url && hasWorkingChainIcon && DASHBOARD_THIRDWEB_SECRET_KEY
? (async () => {
try {
const client = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
});
const response = await download({
client,
uri: chain.icon?.url || "",
});
return response.arrayBuffer();
} catch (error) {
// If download fails, return undefined to fallback to no icon
console.warn("Failed to download chain icon:", error);
return undefined;
}
})()
: undefined,
// download the background image (based on chain)
fetch(
chain.icon?.url && hasWorkingChainIcon
chain.icon?.url && hasWorkingChainIcon && DASHBOARD_THIRDWEB_SECRET_KEY
? new URL(
"og-lib/assets/chain/bg-with-icon.png",

Expand Down Expand Up @@ -118,7 +132,7 @@ export default async function Image({
/>
{/* the actual component starts here */}

{hasWorkingChainIcon && (
{hasWorkingChainIcon && chainIcon && (
<img
alt=""
// @ts-expect-error - TS doesn't know about the ImageResponse component
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getAddress, getContract, isAddress } from "thirdweb";
import { localhost } from "thirdweb/chains";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { mapV4ChainToV5Chain } from "@/utils/map-chains";
import { getUserThirdwebClient } from "../../../../../../../@/api/auth-token";
import { fetchChainWithLocalOverrides } from "../../../../../../../@/utils/fetchChainWithLocalOverrides";
Expand All @@ -18,13 +19,21 @@ export async function getContractPageParamsInfo(params: {
return undefined;
}

// attempt to get the auth token
// Create server client only if secret key is available
if (!DASHBOARD_THIRDWEB_SECRET_KEY) {
return undefined;
}

const serverClient = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
});

const serverContract = getContract({
address: contractAddress,
// eslint-disable-next-line no-restricted-syntax
chain: mapV4ChainToV5Chain(chainMetadata),
client: serverThirdwebClient,
client: serverClient,
});

const clientContract = getContract({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { notFound } from "next/navigation";
import { ImageResponse } from "next/og";
import { resolveAvatar } from "thirdweb/extensions/ens";
import { GradientBlobbie } from "@/components/blocks/avatar/GradientBlobbie";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
/* eslint-disable @next/next/no-img-element */
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
import { shortenIfAddress } from "@/utils/usedapp-external";
Expand All @@ -23,10 +24,18 @@ type PageProps = {

export default async function Image(props: PageProps) {
const params = await props.params;
const resolvedInfo = await resolveAddressAndEns(
params.addressOrEns,
serverThirdwebClient,
);

// Create client only if secret key is available
if (!DASHBOARD_THIRDWEB_SECRET_KEY) {
notFound();
}

const client = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
});

const resolvedInfo = await resolveAddressAndEns(params.addressOrEns, client);

if (!resolvedInfo) {
notFound();
Expand All @@ -43,14 +52,14 @@ export default async function Image(props: PageProps) {

const ensImage = resolvedInfo.ensName
? await resolveAvatar({
client: serverThirdwebClient,
client,
name: resolvedInfo.ensName,
})
: null;

const resolvedENSImageSrc = ensImage
? resolveSchemeWithErrorHandler({
client: serverThirdwebClient,
client,
uri: ensImage,
})
: null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { format } from "date-fns";
import { getSocialProfiles } from "thirdweb/social";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { resolveEns } from "@/lib/ens";
import { correctAndUniqueLicenses } from "@/lib/licenses";
import { getPublishedContractsWithPublisherMapping } from "../utils/getPublishedContractsWithPublisherMapping";
Expand All @@ -22,17 +23,25 @@ export default async function Image(props: {
}) {
const { publisher, contract_id } = props.params;

// Create client only if secret key is available
if (!DASHBOARD_THIRDWEB_SECRET_KEY) {
return null;
}

const client = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
});

const [publishedContracts, socialProfiles] = await Promise.all([
getPublishedContractsWithPublisherMapping({
client: serverThirdwebClient,
client,
contract_id: contract_id,
publisher: publisher,
}),
getSocialProfiles({
address:
(await resolveEns(publisher, serverThirdwebClient)).address ||
publisher,
client: serverThirdwebClient,
address: (await resolveEns(publisher, client)).address || publisher,
client,
}),
]);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { format } from "date-fns";
import { getSocialProfiles } from "thirdweb/social";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { resolveEns } from "@/lib/ens";
import { correctAndUniqueLicenses } from "@/lib/licenses";
import { getLatestPublishedContractsWithPublisherMapping } from "./utils/getPublishedContractsWithPublisherMapping";
Expand All @@ -21,17 +22,25 @@ export default async function Image(props: {
}) {
const { publisher, contract_id } = props.params;

// Create client only if secret key is available
if (!DASHBOARD_THIRDWEB_SECRET_KEY) {
return null;
}

const client = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
});

const [publishedContract, socialProfiles] = await Promise.all([
getLatestPublishedContractsWithPublisherMapping({
client: serverThirdwebClient,
client,
contract_id: contract_id,
publisher: publisher,
}),
getSocialProfiles({
address:
(await resolveEns(publisher, serverThirdwebClient)).address ||
publisher,
client: serverThirdwebClient,
address: (await resolveEns(publisher, client)).address || publisher,
client,
}),
]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { ImageResponse } from "next/og";
import { isAddress } from "thirdweb";
import { download } from "thirdweb/storage";
import { shortenAddress } from "thirdweb/utils";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";

const OgBrandIcon: React.FC = () => (
// biome-ignore lint/a11y/noSvgWithoutTitle: not needed
Expand Down Expand Up @@ -187,17 +188,41 @@ export async function publishedContractOGImageTemplate(params: {
ibmPlexMono500_,
ibmPlexMono700_,
image,
params.logo
? download({
client: serverThirdwebClient,
uri: params.logo,
}).then((res) => res.arrayBuffer())
params.logo && DASHBOARD_THIRDWEB_SECRET_KEY
? (async () => {
try {
const client = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
});
const response = await download({
client,
uri: params.logo || "",
});
return response.arrayBuffer();
} catch (error) {
console.warn("Failed to download logo:", error);
return undefined;
}
})()
: undefined,
params.publisherAvatar
? download({
client: serverThirdwebClient,
uri: params.publisherAvatar,
}).then((res) => res.arrayBuffer())
params.publisherAvatar && DASHBOARD_THIRDWEB_SECRET_KEY
? (async () => {
try {
const client = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
});
const response = await download({
client,
uri: params.publisherAvatar || "",
});
return response.arrayBuffer();
} catch (error) {
console.warn("Failed to download avatar:", error);
return undefined;
}
})()
: undefined,
]);

Expand Down Expand Up @@ -350,7 +375,7 @@ const ERC_CATEGORIES = ["ERC721", "ERC1155", "ERC20"] as const;

function categorizeExtensions(extensions: string[]) {
const categoriesWithCount: Record<
(typeof ERC_CATEGORIES)[number] | "Other",
typeof ERC_CATEGORIES[number] | "Other",
number
> = {
ERC20: 0,
Expand Down
32 changes: 23 additions & 9 deletions apps/dashboard/src/app/(app)/drops/[slug]/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { ImageResponse } from "next/og";
import { useId } from "react";
import { download } from "thirdweb/storage";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { fetchChain } from "@/utils/fetchChain";
import { DROP_PAGES } from "./data";

Expand Down Expand Up @@ -84,16 +85,29 @@ export default async function Image({ params }: { params: { slug: string } }) {
fetch(new URL("og-lib/fonts/inter/700.ttf", import.meta.url)).then((res) =>
res.arrayBuffer(),
),
// download the chain icon if there is one
chain.icon?.url && hasWorkingChainIcon
? download({
client: serverThirdwebClient,
uri: chain.icon.url,
}).then((res) => res.arrayBuffer())
// download the chain icon if there is one and secret key is available
chain.icon?.url && hasWorkingChainIcon && DASHBOARD_THIRDWEB_SECRET_KEY
? (async () => {
try {
const client = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
});
const response = await download({
client,
uri: chain.icon?.url || "",
});
return response.arrayBuffer();
} catch (error) {
// If download fails, return undefined to fallback to no icon
console.warn("Failed to download chain icon:", error);
return undefined;
}
})()
: undefined,
// download the background image (based on chain)
fetch(
chain.icon?.url && hasWorkingChainIcon
chain.icon?.url && hasWorkingChainIcon && DASHBOARD_THIRDWEB_SECRET_KEY
? new URL(
"og-lib/assets/chain/bg-with-icon.png",

Expand Down Expand Up @@ -121,7 +135,7 @@ export default async function Image({ params }: { params: { slug: string } }) {
/>
{/* the actual component starts here */}

{hasWorkingChainIcon && (
{hasWorkingChainIcon && chainIcon && (
<img
alt=""
// @ts-expect-error - TS doesn't know about the ImageResponse component
Expand Down
18 changes: 15 additions & 3 deletions apps/dashboard/src/app/(app)/drops/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
getActiveClaimCondition as getActiveClaimCondition1155,
getNFT as getNFT1155,
} from "thirdweb/extensions/erc1155";
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
import { getConfiguredThirdwebClient } from "@/constants/thirdweb.server";
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
import { defineDashboardChain } from "@/lib/defineDashboardChain";
import { DROP_PAGES } from "./data";
import { NftMint } from "./mint-ui";
Expand Down Expand Up @@ -41,13 +42,24 @@ export default async function DropPage({
if (!project) {
return notFound();
}

// Create client only if secret key is available
if (!DASHBOARD_THIRDWEB_SECRET_KEY) {
return notFound();
}

const client = getConfiguredThirdwebClient({
secretKey: DASHBOARD_THIRDWEB_SECRET_KEY,
teamId: undefined,
});

// eslint-disable-next-line no-restricted-syntax
const chain = defineDashboardChain(project.chainId, undefined);

const contract = getContract({
address: project.contractAddress,
chain,
client: serverThirdwebClient,
client,
});

const [nft, claimCondition, contractMetadata] = await Promise.all([
Expand Down Expand Up @@ -92,7 +104,7 @@ export default async function DropPage({
contract: getContract({
address: claimCondition.currency,
chain,
client: serverThirdwebClient,
client,
}),
})
: undefined;
Expand Down
Loading
Loading