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
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import { Button } from "@workspace/ui/components/button";
import {
BookTextIcon,
BoxIcon,
EllipsisVerticalIcon,
NetworkIcon,
SettingsIcon,
ChevronDownIcon,
CodeIcon,
WebhookIcon,
} from "lucide-react";
import Link from "next/link";
Expand All @@ -17,33 +16,28 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useIsMobile } from "@/hooks/use-mobile";

type LinkType = "api" | "docs" | "playground" | "webhooks" | "settings";
type LinkType = "api" | "docs" | "playground" | "webhooks";

const linkTypeToLabel: Record<LinkType, string> = {
api: "API Reference",
docs: "Documentation",
playground: "Playground",
webhooks: "Webhooks",
settings: "Settings",
};

const linkTypeToOrder: Record<LinkType, number> = {
docs: 0,
playground: 1,
api: 3,
webhooks: 4,
settings: 5,
};

const linkTypeToIcon: Record<LinkType, React.FC<{ className?: string }>> = {
api: NetworkIcon,
api: CodeIcon,
docs: BookTextIcon,
playground: BoxIcon,
webhooks: WebhookIcon,
settings: SettingsIcon,
};

function orderLinks(links: ActionLink[]) {
Expand All @@ -60,52 +54,43 @@ export type ActionLink = {
};

export function LinkGroup(props: { links: ActionLink[] }) {
const isMobile = useIsMobile();
const maxLinks = isMobile ? 1 : 2;
const orderedLinks = useMemo(() => orderLinks(props.links), [props.links]);

// case where we just render directly
if (props.links.length <= maxLinks) {
if (orderedLinks.length === 1 && orderedLinks[0]) {
const link = orderedLinks[0];
const Icon = linkTypeToIcon[link.type];
return (
<div className="flex flex-row items-center gap-2">
{orderedLinks.map((link) => {
const isExternal = link.href.startsWith("http");
const Icon = linkTypeToIcon[link.type];
return (
<ToolTipLabel key={link.type} label={linkTypeToLabel[link.type]}>
<Button
asChild
size="icon"
variant="secondary"
className="rounded-full border"
>
<Link
href={link.href}
target={isExternal ? "_blank" : undefined}
rel={isExternal ? "noopener noreferrer" : undefined}
className="flex flex-row items-center gap-2"
>
<Icon className="size-4 text-foreground" />
</Link>
</Button>
</ToolTipLabel>
);
})}
</div>
<Link
href={link.href}
target={link.href.startsWith("http") ? "_blank" : undefined}
>
<Button
variant="outline"
size="sm"
className="rounded-full border gap-2 bg-card"
>
<Icon className="size-3.5 text-muted-foreground" />
{linkTypeToLabel[link.type]}
</Button>
</Link>
Comment on lines +63 to +75
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix invalid nesting (Link wrapping Button) and add rel for external links

Current markup likely renders , which is invalid and harms accessibility. Use Button asChild to render an anchor styled as a button and add rel for external links.

Apply:

-      <Link
-        href={link.href}
-        target={link.href.startsWith("http") ? "_blank" : undefined}
-      >
-        <Button
-          variant="outline"
-          size="sm"
-          className="rounded-full border gap-2 bg-card"
-        >
-          <Icon className="size-3.5 text-muted-foreground" />
-          {linkTypeToLabel[link.type]}
-        </Button>
-      </Link>
+      <Button
+        asChild
+        variant="outline"
+        size="sm"
+        className="rounded-full border gap-2 bg-card"
+      >
+        <Link
+          href={link.href}
+          target={link.href.startsWith("http") ? "_blank" : undefined}
+          rel={link.href.startsWith("http") ? "noopener noreferrer" : undefined}
+        >
+          <Icon className="size-3.5 text-muted-foreground" />
+          {linkTypeToLabel[link.type]}
+        </Link>
+      </Button>
📝 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
<Link
href={link.href}
target={link.href.startsWith("http") ? "_blank" : undefined}
>
<Button
variant="outline"
size="sm"
className="rounded-full border gap-2 bg-card"
>
<Icon className="size-3.5 text-muted-foreground" />
{linkTypeToLabel[link.type]}
</Button>
</Link>
<Button
asChild
variant="outline"
size="sm"
className="rounded-full border gap-2 bg-card"
>
<Link
href={link.href}
target={link.href.startsWith("http") ? "_blank" : undefined}
rel={link.href.startsWith("http") ? "noopener noreferrer" : undefined}
>
<Icon className="size-3.5 text-muted-foreground" />
{linkTypeToLabel[link.type]}
</Link>
</Button>
🤖 Prompt for AI Agents
In apps/dashboard/src/@/components/blocks/project-page/header/link-group.tsx
around lines 63 to 75, the code currently wraps a Button inside a Link which
produces invalid <a><button/></a> nesting; replace this by rendering the Button
as the anchor (use Button's asChild prop) and forward href/target/rel to the
anchor: remove the outer Link, render Button asChild so it outputs an <a> with
href; set target to "_blank" for external hrefs and add rel="noopener
noreferrer" for those external links; keep Icon and label children unchanged.

);
}

// case where we render a dropdown
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="icon" variant="secondary" className="rounded-full border">
<EllipsisVerticalIcon className="size-4 text-foreground" />
<Button
variant="outline"
size="sm"
className="rounded-full border gap-2 bg-card [&[data-state=open]>svg]:rotate-180"
>
Resources
<ChevronDownIcon className="size-4 transition-transform duration-200 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="center"
className="gap-1 flex flex-col md:w-48 rounded-lg"
align="end"
className="gap-1 flex flex-col w-48 rounded-xl"
sideOffset={10}
>
{orderedLinks.map((link) => {
Expand All @@ -115,7 +100,7 @@ export function LinkGroup(props: { links: ActionLink[] }) {
<DropdownMenuItem
key={link.type}
asChild
className="flex flex-row items-center gap-2 cursor-pointer py-2"
className="flex flex-row items-center gap-2 cursor-pointer py-1.5"
>
<Link
href={link.href}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Button } from "@workspace/ui/components/button";
import { ArrowUpRightIcon } from "lucide-react";
import { ArrowUpRightIcon, Settings2Icon } from "lucide-react";
import Link from "next/link";
import type { ThirdwebClient } from "thirdweb";
import { cn } from "@/lib/utils";
import { ProjectAvatar } from "../avatar/project-avatar";
import { type ActionLink, LinkGroup } from "./header/link-group";

type Action =
Expand Down Expand Up @@ -58,63 +57,99 @@ export type ProjectPageHeaderProps = {
title: string;
description?: React.ReactNode;
imageUrl?: string | null;
icon: React.FC<{ className?: string }>;
isProjectIcon?: boolean;
actions: {
primary: Action;
secondary?: Action;
} | null;

links?: ActionLink[];
settings?: {
href: string;
};

// TODO: add task card component
task?: never;
};

export function ProjectPageHeader(props: ProjectPageHeaderProps) {
return (
<header className="flex flex-col gap-4 container max-w-7xl py-6">
{/* main row */}
<div className="flex flex-row items-center justify-between">
{/* left */}
<div className="flex flex-col gap-4">
{/* image */}
{props.imageUrl !== undefined && (
<ProjectAvatar
className="size-12"
client={props.client}
src={props.imageUrl ?? undefined}
/>
<header className="container max-w-7xl py-6 relative">
{/* top row */}
<div className="flex justify-between items-start mb-4">
{/* left - icon */}
<div className="flex">
{props.isProjectIcon ? (
<props.icon />
) : (
<div className="border rounded-full p-2.5 bg-card">
<props.icon className="size-5 text-muted-foreground" />
</div>
)}
{/* title */}
<div className="flex flex-col gap-1 max-w-3xl">
<h2 className="text-3xl font-semibold tracking-tight line-clamp-1">
{props.title}
</h2>
<p className="text-sm text-muted-foreground line-clamp-3 md:line-clamp-2">
{props.description}
</p>
</div>
</div>

{/* right */}
{/* TODO: add "current task" card component */}
</div>
{/* right - buttons */}
<div className="flex items-center gap-3">
<div className="flex items-center gap-3">
{props.links && props.links.length > 0 && (
<LinkGroup links={props.links} />
)}

{/* actions row */}
{props.actions && (
<div className="flex flex-row items-center justify-between">
{/* left actions */}
<div className="flex flex-row items-center gap-3">
{props.actions.primary && <Action action={props.actions.primary} />}
{props.actions.secondary && (
<Action action={props.actions.secondary} variant="secondary" />
{props.settings && (
<Link href={props.settings.href}>
<Button
variant="outline"
size="sm"
className="rounded-full gap-2 bg-card"
>
<Settings2Icon className="size-4 text-muted-foreground" />
Settings
</Button>
</Link>
)}
</div>
{/* right actions */}
{props.links && props.links.length > 0 && (
<LinkGroup links={props.links} />

{/* hide on mobile */}
{props.actions && (
<div className="hidden lg:flex items-center gap-3">
{props.actions.secondary && (
<Action action={props.actions.secondary} variant="secondary" />
)}

{props.actions.primary && (
<Action action={props.actions.primary} />
)}
</div>
)}
</div>
)}
</div>

<div className="space-y-4">
{/* mid row */}
<div className="space-y-1 max-w-3xl">
<h2 className="text-3xl font-semibold tracking-tight">
{props.title}
</h2>
{/* description */}
<p className="text-sm text-muted-foreground line-clamp-3 md:line-clamp-2">
{props.description}
</p>
</div>

{/* bottom row - hidden on desktop */}
{props.actions && (
<div className="flex items-center gap-3 lg:hidden">
{props.actions?.primary && (
<Action action={props.actions.primary} />
)}

{props.actions?.secondary && (
<Action action={props.actions.secondary} variant="secondary" />
)}
</div>
)}
</div>
</header>
);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { ExternalLinkIcon, ImportIcon } from "lucide-react";
import { ArrowDownToLineIcon, ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
Expand Down Expand Up @@ -234,7 +234,7 @@ function ImportForm(props: {
{addContractToProject.isPending ? (
<Spinner className="size-4" />
) : (
<ImportIcon className="size-4" />
<ArrowDownToLineIcon className="size-4" />
)}

{addContractToProject.isPending ? "Importing" : "Import"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "@/components/analytics/date-range-selector";
import { ProjectPage } from "@/components/blocks/project-page/project-page";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { SmartAccountIcon } from "@/icons/SmartAccountIcon";
import { getAbsoluteUrl } from "@/utils/vercel";
import { AccountAbstractionSummary } from "./AccountAbstractionAnalytics/AccountAbstractionSummary";
import { SmartWalletsBillingAlert } from "./Alerts";
Expand Down Expand Up @@ -93,22 +94,14 @@ export default async function Page(props: {
return (
<ProjectPage
header={{
icon: SmartAccountIcon,
client,
title: "Account Abstraction",
description:
"Integrate EIP-7702 and EIP-4337 compliant smart accounts for gasless sponsorships and more.",

actions: {
primary: {
label: "Documentation",
href: "https://portal.thirdweb.com/transactions/sponsor",
external: true,
},
secondary: {
label: "Playground",
href: "https://playground.thirdweb.com/account-abstraction/eip-7702",
external: true,
},
actions: null,
settings: {
href: `/team/${params.team_slug}/${params.project_slug}/settings/account-abstraction`,
},
links: [
{
Expand All @@ -119,10 +112,6 @@ export default async function Page(props: {
type: "playground",
href: "https://playground.thirdweb.com/account-abstraction/eip-7702",
},
{
type: "settings",
href: `/team/${params.team_slug}/${params.project_slug}/settings/account-abstraction`,
},
],
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { DurationId } from "@/components/analytics/date-range-selector";
import { ResponsiveTimeFilters } from "@/components/analytics/responsive-time-filters";
import { ProjectPage } from "@/components/blocks/project-page/project-page";
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
import { NebulaIcon } from "@/icons/NebulaIcon";
import { getFiltersFromSearchParams } from "@/lib/time";
import { loginRedirect } from "@/utils/redirects";
import { AiAnalytics } from "./analytics/chart";
Expand Down Expand Up @@ -57,21 +58,11 @@ export default async function Page(props: {
<ResponsiveSearchParamsProvider value={searchParams}>
<ProjectPage
header={{
icon: NebulaIcon,
client,
title: "AI",
description: "Interact with any EVM chain with natural language",
actions: {
primary: {
label: "Documentation",
href: "https://portal.thirdweb.com/ai/chat",
external: true,
},
secondary: {
label: "API Reference",
href: "https://api.thirdweb.com/reference#tag/ai/post/ai/chat",
external: true,
},
},
actions: null,
links: [
{
href: "https://portal.thirdweb.com/ai/chat",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
HomeIcon,
LockIcon,
RssIcon,
SettingsIcon,
Settings2Icon,
WebhookIcon,
} from "lucide-react";
import { FullWidthSidebarLayout } from "@/components/blocks/full-width-sidebar-layout";
Expand Down Expand Up @@ -138,7 +138,7 @@ export function ProjectSidebarLayout(props: {
},
{
href: `${props.layoutPath}/settings`,
icon: SettingsIcon,
icon: Settings2Icon,
label: "Project Settings",
},
{
Expand Down
Loading
Loading