Skip to content

Commit 07af647

Browse files
committed
Migrate profile page to tailwind and Move to app router and use RSCs (#5364)
## Problem solved Short description of the bug fixed or feature added <!-- start pr-codex --> --- ## PR-Codex overview This PR primarily focuses on updating the profile management features within the application. It introduces new functionalities for resolving addresses and fetching published contracts while enhancing the user interface for profile editing. ### Detailed summary - Removed unused files from the project. - Updated `replaceDeployerAddress` to include a specific address replacement. - Changed `PlaygroundMenu` to use `useDashboardRouter`. - Enhanced `getSortedDeployedContracts` to handle mainnet filtering. - Made `mapThirdwebPublisher` an exportable function. - Added `resolveAddressAndEns` for address resolution. - Introduced `PublishedContracts` component to display published contracts. - Modified `DeployedContracts` to simplify contract listing logic. - Updated `ProfileUI` to fetch and display publisher profiles. - Enhanced `EditProfile` component for better profile editing experience. - Improved `PublisherSocials` to streamline social media links. - Added loading states and error handling in various components. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 535e1df commit 07af647

File tree

18 files changed

+688
-865
lines changed

18 files changed

+688
-865
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { Spinner } from "@/components/ui/Spinner/Spinner";
2+
import { fetchPublishedContracts } from "components/contract-components/fetchPublishedContracts";
3+
import { PublisherSocials } from "components/contract-components/publisher/PublisherSocials";
4+
import { EditProfile } from "components/contract-components/publisher/edit-profile";
5+
import { PublisherAvatar } from "components/contract-components/publisher/masked-avatar";
6+
import { DeployedContracts } from "components/contract-components/tables/deployed-contracts";
7+
import type { ProfileMetadata } from "constants/schemas";
8+
import { Suspense } from "react";
9+
import { shortenIfAddress } from "utils/usedapp-external";
10+
import { getSortedDeployedContracts } from "../../../account/contracts/_components/getSortedDeployedContracts";
11+
import { PublishedContracts } from "./components/published-contracts";
12+
13+
export function ProfileUI(props: {
14+
profileAddress: string;
15+
ensName: string | undefined;
16+
publisherProfile: ProfileMetadata;
17+
showEditProfile: boolean;
18+
}) {
19+
const { profileAddress, ensName, publisherProfile, showEditProfile } = props;
20+
21+
const displayName = shortenIfAddress(ensName || profileAddress).replace(
22+
"deployer.thirdweb.eth",
23+
"thirdweb.eth",
24+
);
25+
26+
return (
27+
<div className="container pt-8 pb-20">
28+
{/* Header */}
29+
<div className="flex w-full flex-col items-center justify-between gap-4 border-border border-b pb-6 md:flex-row">
30+
<div className="flex w-full items-center gap-4">
31+
<PublisherAvatar address={profileAddress} className="size-20" />
32+
<div>
33+
<h1 className="font-semibold text-4xl tracking-tight">
34+
{displayName}
35+
</h1>
36+
37+
{publisherProfile.bio && (
38+
<p className="line-clamp-2 text-muted-foreground">
39+
{publisherProfile.bio}
40+
</p>
41+
)}
42+
43+
<div className="-translate-x-2 mt-1">
44+
<PublisherSocials publisherProfile={publisherProfile} />
45+
</div>
46+
</div>
47+
</div>
48+
49+
{showEditProfile && (
50+
<div className="shrink-0">
51+
<EditProfile publisherProfile={publisherProfile} />
52+
</div>
53+
)}
54+
</div>
55+
56+
<div className="h-8" />
57+
58+
<div>
59+
<h2 className="font-semibold text-2xl tracking-tight">
60+
Published contracts
61+
</h2>
62+
63+
<div className="h-4" />
64+
<Suspense fallback={<LoadingSection />}>
65+
<AsyncPublishedContracts
66+
publisherAddress={profileAddress}
67+
publisherEnsName={ensName}
68+
/>
69+
</Suspense>
70+
</div>
71+
72+
<div className="h-12" />
73+
74+
<div>
75+
<h2 className="font-semibold text-2xl tracking-tight">
76+
Deployed contracts
77+
</h2>
78+
79+
<p className="text-muted-foreground">
80+
List of contracts deployed across all Mainnets
81+
</p>
82+
83+
<div className="h-4" />
84+
<Suspense fallback={<LoadingSection />}>
85+
<AsyncDeployedContracts profileAddress={profileAddress} />
86+
</Suspense>
87+
</div>
88+
</div>
89+
);
90+
}
91+
92+
async function AsyncDeployedContracts(props: {
93+
profileAddress: string;
94+
}) {
95+
const contracts = await getSortedDeployedContracts({
96+
address: props.profileAddress,
97+
onlyMainnet: true,
98+
});
99+
100+
return <DeployedContracts contractList={contracts} limit={50} />;
101+
}
102+
103+
async function AsyncPublishedContracts(props: {
104+
publisherAddress: string;
105+
publisherEnsName: string | undefined;
106+
}) {
107+
const publishedContracts = await fetchPublishedContracts(
108+
props.publisherAddress,
109+
);
110+
111+
if (publishedContracts.length === 0) {
112+
return (
113+
<div className="flex min-h-[300px] items-center justify-center rounded-lg border border-border">
114+
No published contracts found
115+
</div>
116+
);
117+
}
118+
119+
return (
120+
<PublishedContracts
121+
publishedContracts={publishedContracts}
122+
publisherEnsName={props.publisherEnsName}
123+
/>
124+
);
125+
}
126+
127+
function LoadingSection() {
128+
return (
129+
<div className="flex min-h-[450px] items-center justify-center rounded-lg border border-border">
130+
<Spinner className="size-10" />
131+
</div>
132+
);
133+
}
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { Img } from "@/components/blocks/Img";
2+
import { Button } from "@/components/ui/button";
3+
import {
4+
Table,
5+
TableBody,
6+
TableCell,
7+
TableContainer,
8+
TableHead,
9+
TableHeader,
10+
TableRow,
11+
} from "@/components/ui/table";
12+
import { ToolTipLabel } from "@/components/ui/tooltip";
13+
import { TrackedLinkTW } from "@/components/ui/tracked-link";
14+
import { replaceDeployerAddress } from "components/explore/publisher";
15+
import { useTrack } from "hooks/analytics/useTrack";
16+
import { replaceIpfsUrl } from "lib/sdk";
17+
import { ShieldCheckIcon } from "lucide-react";
18+
import Link from "next/link";
19+
import { useMemo } from "react";
20+
import { type Column, type Row, useTable } from "react-table";
21+
import type { PublishedContractDetails } from "../../../../../components/contract-components/hooks";
22+
23+
interface PublishedContractTableProps {
24+
contractDetails: ContractDataInput[];
25+
footer?: React.ReactNode;
26+
publisherEnsName: string | undefined;
27+
}
28+
29+
type ContractDataInput = PublishedContractDetails;
30+
type ContractDataRow = ContractDataInput["metadata"] & {
31+
id: string;
32+
};
33+
34+
function convertContractDataToRowData(
35+
input: ContractDataInput,
36+
): ContractDataRow {
37+
return {
38+
id: input.contractId,
39+
...input.metadata,
40+
};
41+
}
42+
43+
export function PublishedContractTable(props: PublishedContractTableProps) {
44+
const { contractDetails, footer, publisherEnsName } = props;
45+
const trackEvent = useTrack();
46+
const rows = useMemo(
47+
() => contractDetails.map(convertContractDataToRowData),
48+
[contractDetails],
49+
);
50+
51+
const tableColumns: Column<ContractDataRow>[] = useMemo(() => {
52+
const cols: Column<ContractDataRow>[] = [
53+
{
54+
Header: "Logo",
55+
accessor: (row) => row.logo,
56+
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
57+
Cell: (cell: any) => (
58+
<Img
59+
alt=""
60+
src={cell.value ? replaceIpfsUrl(cell.value) : ""}
61+
fallback={
62+
<div className="size-8 rounded-full border border-border bg-muted" />
63+
}
64+
className="size-8"
65+
/>
66+
),
67+
},
68+
{
69+
Header: "Name",
70+
accessor: (row) => row.name,
71+
// biome-ignore lint/suspicious/noExplicitAny: FIXME
72+
Cell: (cell: any) => {
73+
return (
74+
<Link
75+
href={replaceDeployerAddress(
76+
`/${publisherEnsName || cell.row.original.publisher}/${cell.row.original.id}`,
77+
)}
78+
className="whitespace-nowrap text-foreground before:absolute before:inset-0"
79+
>
80+
{cell.value}
81+
</Link>
82+
);
83+
},
84+
},
85+
{
86+
Header: "Description",
87+
accessor: (row) => row.description,
88+
// biome-ignore lint/suspicious/noExplicitAny: FIXME
89+
Cell: (cell: any) => (
90+
<span className="line-clamp-2 text-muted-foreground">
91+
{cell.value}
92+
</span>
93+
),
94+
},
95+
{
96+
Header: "Version",
97+
accessor: (row) => row.version,
98+
// biome-ignore lint/suspicious/noExplicitAny: FIXME
99+
Cell: (cell: any) => (
100+
<span className="text-muted-foreground">{cell.value}</span>
101+
),
102+
},
103+
{
104+
id: "audit-badge",
105+
accessor: (row) => ({ audit: row.audit }),
106+
// biome-ignore lint/suspicious/noExplicitAny: FIXME
107+
Cell: (cell: any) => (
108+
<span className="flex items-center gap-2">
109+
{cell.value.audit ? (
110+
<ToolTipLabel label="View Contract Audit">
111+
<Button
112+
asChild
113+
variant="ghost"
114+
className="relative z-10 h-auto w-auto p-2"
115+
>
116+
<TrackedLinkTW
117+
href={replaceIpfsUrl(cell.value.audit)}
118+
category="deploy"
119+
label="audited"
120+
aria-label="View Contract Audit"
121+
target="_blank"
122+
onClick={(e) => {
123+
e.stopPropagation();
124+
trackEvent({
125+
category: "visit-audit",
126+
action: "click",
127+
label: cell.value.audit,
128+
});
129+
}}
130+
>
131+
<ShieldCheckIcon className="size-5 text-success-text" />
132+
</TrackedLinkTW>
133+
</Button>
134+
</ToolTipLabel>
135+
) : null}
136+
</span>
137+
),
138+
},
139+
];
140+
141+
return cols;
142+
}, [trackEvent, publisherEnsName]);
143+
144+
const tableInstance = useTable({
145+
columns: tableColumns,
146+
data: rows,
147+
});
148+
149+
return (
150+
<TableContainer>
151+
<Table {...tableInstance.getTableProps()}>
152+
<TableHeader>
153+
{tableInstance.headerGroups.map((headerGroup) => {
154+
const { key, ...rowProps } = headerGroup.getHeaderGroupProps();
155+
return (
156+
<TableRow {...rowProps} key={key}>
157+
{headerGroup.headers.map((column, columnIndex) => (
158+
<TableHead
159+
{...column.getHeaderProps()}
160+
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
161+
key={columnIndex}
162+
>
163+
<span className="text-muted-foreground">
164+
{column.render("Header")}
165+
</span>
166+
</TableHead>
167+
))}
168+
</TableRow>
169+
);
170+
})}
171+
</TableHeader>
172+
173+
<TableBody {...tableInstance.getTableBodyProps()} className="relative">
174+
{tableInstance.rows.map((row) => {
175+
tableInstance.prepareRow(row);
176+
return <ContractTableRow row={row} key={row.getRowProps().key} />;
177+
})}
178+
</TableBody>
179+
</Table>
180+
{footer}
181+
</TableContainer>
182+
);
183+
}
184+
185+
function ContractTableRow(props: {
186+
row: Row<ContractDataRow>;
187+
}) {
188+
const { row } = props;
189+
const { key, ...rowProps } = row.getRowProps();
190+
return (
191+
<>
192+
<TableRow
193+
className="relative cursor-pointer hover:bg-muted/50"
194+
{...rowProps}
195+
key={key}
196+
>
197+
{row.cells.map((cell) => (
198+
<TableCell {...cell.getCellProps()} key={cell.getCellProps().key}>
199+
{cell.render("Cell")}
200+
</TableCell>
201+
))}
202+
</TableRow>
203+
</>
204+
);
205+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import type { fetchPublishedContracts } from "../../../../../components/contract-components/fetchPublishedContracts";
5+
import { ShowMoreButton } from "../../../../../components/contract-components/tables/show-more-button";
6+
import { PublishedContractTable } from "./PublishedContractTable";
7+
8+
interface PublishedContractsProps {
9+
limit?: number;
10+
publishedContracts: Awaited<ReturnType<typeof fetchPublishedContracts>>;
11+
publisherEnsName: string | undefined;
12+
}
13+
14+
export const PublishedContracts: React.FC<PublishedContractsProps> = ({
15+
limit = 10,
16+
publishedContracts,
17+
publisherEnsName,
18+
}) => {
19+
const [showMoreLimit, setShowMoreLimit] = useState(10);
20+
const slicedData = publishedContracts.slice(0, showMoreLimit);
21+
22+
return (
23+
<PublishedContractTable
24+
contractDetails={slicedData}
25+
publisherEnsName={publisherEnsName}
26+
footer={
27+
publishedContracts.length > slicedData.length ? (
28+
<ShowMoreButton
29+
limit={limit}
30+
showMoreLimit={showMoreLimit}
31+
setShowMoreLimit={setShowMoreLimit}
32+
/>
33+
) : undefined
34+
}
35+
/>
36+
);
37+
};

0 commit comments

Comments
 (0)