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
@@ -0,0 +1,133 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { fetchPublishedContracts } from "components/contract-components/fetchPublishedContracts";
import { PublisherSocials } from "components/contract-components/publisher/PublisherSocials";
import { EditProfile } from "components/contract-components/publisher/edit-profile";
import { PublisherAvatar } from "components/contract-components/publisher/masked-avatar";
import { DeployedContracts } from "components/contract-components/tables/deployed-contracts";
import type { ProfileMetadata } from "constants/schemas";
import { Suspense } from "react";
import { shortenIfAddress } from "utils/usedapp-external";
import { getSortedDeployedContracts } from "../../../account/contracts/_components/getSortedDeployedContracts";
import { PublishedContracts } from "./components/published-contracts";

export function ProfileUI(props: {
profileAddress: string;
ensName: string | undefined;
publisherProfile: ProfileMetadata;
showEditProfile: boolean;
}) {
const { profileAddress, ensName, publisherProfile, showEditProfile } = props;

const displayName = shortenIfAddress(ensName || profileAddress).replace(
"deployer.thirdweb.eth",
"thirdweb.eth",
);

return (
<div className="container pt-8 pb-20">
{/* Header */}
<div className="flex w-full flex-col items-center justify-between gap-4 border-border border-b pb-6 md:flex-row">
<div className="flex w-full items-center gap-4">
<PublisherAvatar address={profileAddress} className="size-20" />
<div>
<h1 className="font-semibold text-4xl tracking-tight">
{displayName}
</h1>

{publisherProfile.bio && (
<p className="line-clamp-2 text-muted-foreground">
{publisherProfile.bio}
</p>
)}

<div className="-translate-x-2 mt-1">
<PublisherSocials publisherProfile={publisherProfile} />
</div>
</div>
</div>

{showEditProfile && (
<div className="shrink-0">
<EditProfile publisherProfile={publisherProfile} />
</div>
)}
</div>

<div className="h-8" />

<div>
<h2 className="font-semibold text-2xl tracking-tight">
Published contracts
</h2>

<div className="h-4" />
<Suspense fallback={<LoadingSection />}>
<AsyncPublishedContracts
publisherAddress={profileAddress}
publisherEnsName={ensName}
/>
</Suspense>
</div>

<div className="h-12" />

<div>
<h2 className="font-semibold text-2xl tracking-tight">
Deployed contracts
</h2>

<p className="text-muted-foreground">
List of contracts deployed across all Mainnets
</p>

<div className="h-4" />
<Suspense fallback={<LoadingSection />}>
<AsyncDeployedContracts profileAddress={profileAddress} />
</Suspense>
</div>
</div>
);
}

async function AsyncDeployedContracts(props: {
profileAddress: string;
}) {
const contracts = await getSortedDeployedContracts({
address: props.profileAddress,
onlyMainnet: true,
});

return <DeployedContracts contractList={contracts} limit={50} />;
}

async function AsyncPublishedContracts(props: {
publisherAddress: string;
publisherEnsName: string | undefined;
}) {
const publishedContracts = await fetchPublishedContracts(
props.publisherAddress,
);

if (publishedContracts.length === 0) {
return (
<div className="flex min-h-[300px] items-center justify-center rounded-lg border border-border">
No published contracts found
</div>
);
}

return (
<PublishedContracts
publishedContracts={publishedContracts}
publisherEnsName={props.publisherEnsName}
/>
);
}

function LoadingSection() {
return (
<div className="flex min-h-[450px] items-center justify-center rounded-lg border border-border">
<Spinner className="size-10" />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import { Img } from "@/components/blocks/Img";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { replaceDeployerAddress } from "components/explore/publisher";
import { useTrack } from "hooks/analytics/useTrack";
import { replaceIpfsUrl } from "lib/sdk";
import { ShieldCheckIcon } from "lucide-react";
import Link from "next/link";
import { useMemo } from "react";
import { type Column, type Row, useTable } from "react-table";
import type { PublishedContractDetails } from "../../../../../components/contract-components/hooks";

interface PublishedContractTableProps {
contractDetails: ContractDataInput[];
footer?: React.ReactNode;
publisherEnsName: string | undefined;
}

type ContractDataInput = PublishedContractDetails;
type ContractDataRow = ContractDataInput["metadata"] & {
id: string;
};

function convertContractDataToRowData(
input: ContractDataInput,
): ContractDataRow {
return {
id: input.contractId,
...input.metadata,
};
}

export function PublishedContractTable(props: PublishedContractTableProps) {
const { contractDetails, footer, publisherEnsName } = props;
const trackEvent = useTrack();
const rows = useMemo(
() => contractDetails.map(convertContractDataToRowData),
[contractDetails],
);

const tableColumns: Column<ContractDataRow>[] = useMemo(() => {
const cols: Column<ContractDataRow>[] = [
{
Header: "Logo",
accessor: (row) => row.logo,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
Cell: (cell: any) => (
<Img
alt=""
src={cell.value ? replaceIpfsUrl(cell.value) : ""}
fallback={
<div className="size-8 rounded-full border border-border bg-muted" />
}
className="size-8"
/>
),
},
{
Header: "Name",
accessor: (row) => row.name,
// biome-ignore lint/suspicious/noExplicitAny: FIXME
Cell: (cell: any) => {
return (
<Link
href={replaceDeployerAddress(
`/${publisherEnsName || cell.row.original.publisher}/${cell.row.original.id}`,
)}
className="whitespace-nowrap text-foreground before:absolute before:inset-0"
>
{cell.value}
</Link>
);
},
},
{
Header: "Description",
accessor: (row) => row.description,
// biome-ignore lint/suspicious/noExplicitAny: FIXME
Cell: (cell: any) => (
<span className="line-clamp-2 text-muted-foreground">
{cell.value}
</span>
),
},
{
Header: "Version",
accessor: (row) => row.version,
// biome-ignore lint/suspicious/noExplicitAny: FIXME
Cell: (cell: any) => (
<span className="text-muted-foreground">{cell.value}</span>
),
},
{
id: "audit-badge",
accessor: (row) => ({ audit: row.audit }),
// biome-ignore lint/suspicious/noExplicitAny: FIXME
Cell: (cell: any) => (
<span className="flex items-center gap-2">
{cell.value.audit ? (
<ToolTipLabel label="View Contract Audit">
<Button
asChild
variant="ghost"
className="relative z-10 h-auto w-auto p-2"
>
<TrackedLinkTW
href={replaceIpfsUrl(cell.value.audit)}
category="deploy"
label="audited"
aria-label="View Contract Audit"
target="_blank"
onClick={(e) => {
e.stopPropagation();
trackEvent({
category: "visit-audit",
action: "click",
label: cell.value.audit,
});
}}
>
<ShieldCheckIcon className="size-5 text-success-text" />
</TrackedLinkTW>
</Button>
</ToolTipLabel>
) : null}
</span>
),
},
];

return cols;
}, [trackEvent, publisherEnsName]);

const tableInstance = useTable({
columns: tableColumns,
data: rows,
});

return (
<TableContainer>
<Table {...tableInstance.getTableProps()}>
<TableHeader>
{tableInstance.headerGroups.map((headerGroup) => {
const { key, ...rowProps } = headerGroup.getHeaderGroupProps();
return (
<TableRow {...rowProps} key={key}>
{headerGroup.headers.map((column, columnIndex) => (
<TableHead
{...column.getHeaderProps()}
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
key={columnIndex}
>
<span className="text-muted-foreground">
{column.render("Header")}
</span>
</TableHead>
))}
</TableRow>
);
})}
</TableHeader>

<TableBody {...tableInstance.getTableBodyProps()} className="relative">
{tableInstance.rows.map((row) => {
tableInstance.prepareRow(row);
return <ContractTableRow row={row} key={row.getRowProps().key} />;
})}
</TableBody>
</Table>
{footer}
</TableContainer>
);
}

function ContractTableRow(props: {
row: Row<ContractDataRow>;
}) {
const { row } = props;
const { key, ...rowProps } = row.getRowProps();
return (
<>
<TableRow
className="relative cursor-pointer hover:bg-muted/50"
{...rowProps}
key={key}
>
{row.cells.map((cell) => (
<TableCell {...cell.getCellProps()} key={cell.getCellProps().key}>
{cell.render("Cell")}
</TableCell>
))}
</TableRow>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client";

import { useState } from "react";
import type { fetchPublishedContracts } from "../../../../../components/contract-components/fetchPublishedContracts";
import { ShowMoreButton } from "../../../../../components/contract-components/tables/show-more-button";
import { PublishedContractTable } from "./PublishedContractTable";

interface PublishedContractsProps {
limit?: number;
publishedContracts: Awaited<ReturnType<typeof fetchPublishedContracts>>;
publisherEnsName: string | undefined;
}

export const PublishedContracts: React.FC<PublishedContractsProps> = ({
limit = 10,
publishedContracts,
publisherEnsName,
}) => {
const [showMoreLimit, setShowMoreLimit] = useState(10);
const slicedData = publishedContracts.slice(0, showMoreLimit);

return (
<PublishedContractTable
contractDetails={slicedData}
publisherEnsName={publisherEnsName}
footer={
publishedContracts.length > slicedData.length ? (
<ShowMoreButton
limit={limit}
showMoreLimit={showMoreLimit}
setShowMoreLimit={setShowMoreLimit}
/>
) : undefined
}
/>
);
};
Loading
Loading