refactor(frontend): Revamp creator page data fetching and structure (#10737)

### Changes 🏗️
- Updated the creator page to utilize React Query for data fetching,
improving performance and reliability.
- Removed legacy API calls and integrated prefetching for creator
details and agents.
- Introduced a new MainCreatorPage component for better separation of
concerns.
- Added a hydration boundary for managing server state.

### Checklist 📋

### Checklist 📋
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] All marketplace E2E tests are working.
- [x] I’ve tested all the links and checked if everything renders
perfectly on the marketplace page.
This commit is contained in:
Abhimanyu Yadav
2025-08-27 09:34:57 +05:30
committed by GitHub
parent c6821484c7
commit 533d2d0277
8 changed files with 409 additions and 111 deletions

View File

@@ -0,0 +1,115 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { StarRatingIcons } from "@/components/ui/icons";
interface CreatorInfoCardProps {
username: string;
handle: string;
avatarSrc: string;
categories: string[];
averageRating: number;
totalRuns: number;
}
export const CreatorInfoCard = ({
username,
handle,
avatarSrc,
categories,
averageRating,
totalRuns,
}: CreatorInfoCardProps) => {
return (
<div
className="inline-flex h-auto min-h-[500px] w-full max-w-[440px] flex-col items-start justify-between rounded-[26px] bg-violet-100 p-4 dark:bg-violet-900 sm:h-[632px] sm:w-[440px] sm:p-6"
role="article"
aria-label={`Creator profile for ${username}`}
>
<div className="flex w-full flex-col items-start justify-start gap-3.5 sm:h-[218px]">
<Avatar className="h-[100px] w-[100px] sm:h-[130px] sm:w-[130px]">
<AvatarImage
width={130}
height={130}
src={avatarSrc}
alt={`${username}'s avatar`}
/>
<AvatarFallback
size={130}
className="h-[100px] w-[100px] sm:h-[130px] sm:w-[130px]"
>
{username.charAt(0)}
</AvatarFallback>
</Avatar>
<div className="flex w-full flex-col items-start justify-start gap-1.5">
<div
data-testid="creator-title"
className="w-full font-poppins text-[35px] font-medium leading-10 text-neutral-900 dark:text-neutral-100 sm:text-[35px] sm:leading-10"
>
{username}
</div>
<div className="w-full text-lg font-normal leading-6 text-neutral-800 dark:text-neutral-200 sm:text-xl sm:leading-7">
@{handle}
</div>
</div>
</div>
<div className="my-4 flex w-full flex-col items-start justify-start gap-6 sm:gap-[50px]">
<div className="flex w-full flex-col items-start justify-start gap-3">
<div className="h-px w-full bg-neutral-700 dark:bg-neutral-300" />
<div className="flex flex-col items-start justify-start gap-2.5">
<div className="w-full text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
Top categories
</div>
<div
className="flex flex-wrap items-center gap-2.5"
role="list"
aria-label="Categories"
>
{categories.map((category, index) => (
<div
key={index}
className="flex items-center justify-center gap-2.5 rounded-[34px] border border-neutral-600 px-4 py-3 dark:border-neutral-400"
role="listitem"
>
<div className="text-base font-normal leading-normal text-neutral-800 dark:text-neutral-200">
{category}
</div>
</div>
))}
</div>
</div>
</div>
<div className="flex w-full flex-col items-start justify-start gap-3">
<div className="h-px w-full bg-neutral-700 dark:bg-neutral-300" />
<div className="flex w-full flex-col items-start justify-between gap-4 sm:flex-row sm:gap-0">
<div className="flex w-full flex-col items-start justify-start gap-2.5 sm:w-[164px]">
<div className="w-full text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
Average rating
</div>
<div className="inline-flex items-center gap-2">
<div className="text-[18px] font-semibold leading-[28px] text-neutral-800 dark:text-neutral-200">
{averageRating.toFixed(1)}
</div>
<div
className="flex items-center gap-px"
role="img"
aria-label={`Rating: ${averageRating} out of 5 stars`}
>
{StarRatingIcons(averageRating)}
</div>
</div>
</div>
<div className="flex w-full flex-col items-start justify-start gap-2.5 sm:w-[164px]">
<div className="w-full text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
Number of runs
</div>
<div className="text-[18px] font-semibold leading-[28px] text-neutral-800 dark:text-neutral-200">
{new Intl.NumberFormat().format(totalRuns)} runs
</div>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,43 @@
import { getIconForSocial } from "@/components/ui/icons";
import { Fragment } from "react";
interface CreatorLinksProps {
links: string[];
}
export const CreatorLinks = ({ links }: CreatorLinksProps) => {
if (!links || links.length === 0) {
return null;
}
const renderLinkButton = (url: string) => (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="flex min-w-[200px] flex-1 items-center justify-between rounded-[34px] border border-neutral-600 px-5 py-3 dark:border-neutral-400"
>
<div className="text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
{new URL(url).hostname.replace("www.", "")}
</div>
<div className="relative h-6 w-6">
{getIconForSocial(url, {
className: "h-6 w-6 text-neutral-800 dark:text-neutral-200",
})}
</div>
</a>
);
return (
<div className="flex flex-col items-start justify-start gap-4">
<div className="text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
Other links
</div>
<div className="flex w-full flex-wrap gap-3">
{links.map((link, index) => (
<Fragment key={index}>{renderLinkButton(link)}</Fragment>
))}
</div>
</div>
);
};

View File

@@ -0,0 +1,40 @@
import { Skeleton } from "@/components/ui/skeleton";
export const CreatorPageLoading = () => {
return (
<div className="mx-auto w-screen max-w-[1360px]">
<main className="mt-5 px-4">
<Skeleton className="mb-4 h-6 w-40" />
<div className="mt-4 flex flex-col items-start gap-4 sm:mt-6 sm:gap-6 md:mt-8 md:flex-row md:gap-8">
<div className="w-full md:w-auto md:shrink-0">
<Skeleton className="h-80 w-80 rounded-xl" />
<div className="mt-4 space-y-2">
<Skeleton className="h-6 w-80" />
<Skeleton className="h-4 w-80" />
</div>
</div>
<div className="flex min-w-0 flex-1 flex-col gap-4">
<Skeleton className="h-6 w-24" />
<Skeleton className="h-8 w-full max-w-xl" />
<Skeleton className="h-4 w-1/2" />
<div className="flex gap-2">
<Skeleton className="h-8 w-8 rounded-full" />
<Skeleton className="h-8 w-8 rounded-full" />
</div>
</div>
</div>
<div className="mt-8">
<Skeleton className="mb-6 h-px w-full" />
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className="h-32 w-full rounded-lg" />
))}
</div>
</div>
</main>
</div>
);
};

View File

@@ -0,0 +1,90 @@
"use client";
import { Separator } from "@/components/ui/separator";
import { AgentsSection } from "../AgentsSection/AgentsSection";
import { MarketplaceCreatorPageParams } from "../../creator/[creator]/page";
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
import { CreatorInfoCard } from "../CreatorInfoCard/CreatorInfoCard";
import { CreatorLinks } from "../CreatorLinks/CreatorLinks";
import { useMainCreatorPage } from "./useMainCreatorPage";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { CreatorPageLoading } from "../CreatorPageLoading";
interface MainCreatorPageProps {
params: MarketplaceCreatorPageParams;
}
export const MainCreatorPage = ({ params }: MainCreatorPageProps) => {
const { creatorAgents, creator, isLoading, hasError } = useMainCreatorPage({
params,
});
if (isLoading) return <CreatorPageLoading />;
if (hasError) {
return (
<div className="mx-auto w-screen max-w-[1360px]">
<div className="flex min-h-[60vh] items-center justify-center">
<ErrorCard
isSuccess={false}
responseError={{ message: "Failed to load creator data" }}
context="creator page"
onRetry={() => window.location.reload()}
className="w-full max-w-md"
/>
</div>
</div>
);
}
if (creator)
return (
<div className="mx-auto w-screen max-w-[1360px]">
<main className="mt-5 px-4">
<Breadcrumbs
items={[
{ name: "Store", link: "/marketplace" },
{ name: creator.name, link: "#" },
]}
/>
<div className="mt-4 flex flex-col items-start gap-4 sm:mt-6 sm:gap-6 md:mt-8 md:flex-row md:gap-8">
<div className="w-full md:w-auto md:shrink-0">
<CreatorInfoCard
username={creator.name}
handle={creator.username}
avatarSrc={creator.avatar_url}
categories={creator.top_categories}
averageRating={creator.agent_rating}
totalRuns={creator.agent_runs}
/>
</div>
<div className="flex min-w-0 flex-1 flex-col gap-4 sm:gap-6 md:gap-8">
<p className="text-underline-position-from-font text-decoration-skip-none text-left font-poppins text-base font-medium leading-6">
About
</p>
<div
className="text-[48px] font-normal leading-[59px] text-neutral-900 dark:text-zinc-50"
style={{ whiteSpace: "pre-line" }}
data-testid="creator-description"
>
{creator.description}
</div>
<CreatorLinks links={creator.links} />
</div>
</div>
<div className="mt-8 sm:mt-12 md:mt-16 lg:pb-[58px]">
<Separator className="mb-6 bg-gray-200" />
{creatorAgents && (
<AgentsSection
agents={creatorAgents.agents}
hideAvatars={true}
sectionTitle={`Agents by ${creator.name}`}
/>
)}
</div>
</main>
</div>
);
};

View File

@@ -0,0 +1,50 @@
import {
useGetV2GetCreatorDetails,
useGetV2ListStoreAgents,
} from "@/app/api/__generated__/endpoints/store/store";
import { StoreAgentsResponse } from "@/app/api/__generated__/models/storeAgentsResponse";
import { MarketplaceCreatorPageParams } from "../../creator/[creator]/page";
import { CreatorDetails } from "@/app/api/__generated__/models/creatorDetails";
interface useMainCreatorPageProps {
params: MarketplaceCreatorPageParams;
}
export const useMainCreatorPage = ({ params }: useMainCreatorPageProps) => {
const {
data: creatorAgents,
isLoading: isCreatorAgentsLoading,
isError: isCreatorAgentsError,
} = useGetV2ListStoreAgents(
{ creator: params.creator },
{
query: {
select: (x) => {
return x.data as StoreAgentsResponse;
},
},
},
);
const {
data: creator,
isLoading: isCreatorDetailsLoading,
isError: isCreatorDetailsError,
} = useGetV2GetCreatorDetails(params.creator, {
query: {
select: (x) => {
return x.data as CreatorDetails;
},
},
});
const isLoading = isCreatorAgentsLoading || isCreatorDetailsLoading;
const hasError = isCreatorAgentsError || isCreatorDetailsError;
return {
creatorAgents,
creator,
isLoading,
hasError,
};
};

View File

@@ -6,52 +6,29 @@ import { HeroSection } from "../HeroSection/HeroSection";
import { AgentsSection } from "../AgentsSection/AgentsSection";
import { useMainMarketplacePage } from "./useMainMarketplacePage";
import { FeaturedCreators } from "../FeaturedCreators/FeaturedCreators";
import { Skeleton } from "@/components/ui/skeleton";
import { MainMarketplacePageLoading } from "../MainMarketplacePageLoading";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
export const MainMarkeplacePage = () => {
const { featuredAgents, topAgents, featuredCreators, isLoading, hasError } =
useMainMarketplacePage();
// FRONTEND-TODO : Add better Loading Skeletons
if (isLoading) {
return (
<div className="mx-auto w-screen max-w-[1360px]">
<main className="px-4">
<div className="flex flex-col gap-2 pt-16">
<div className="flex flex-col items-center justify-center gap-8">
<Skeleton className="h-16 w-[60%]" />
<Skeleton className="h-12 w-[40%]" />
</div>
<div className="flex flex-col items-center justify-center gap-8 pt-8">
<Skeleton className="h-8 w-[60%]" />
</div>
<div className="mx-auto flex w-[80%] flex-wrap items-center justify-center gap-8 pt-24">
<Skeleton className="h-[12rem] w-[12rem]" />
<Skeleton className="h-[12rem] w-[12rem]" />
<Skeleton className="h-[12rem] w-[12rem]" />
<Skeleton className="h-[12rem] w-[12rem]" />
<Skeleton className="h-[12rem] w-[12rem]" />
<Skeleton className="h-[12rem] w-[12rem]" />
<Skeleton className="h-[12rem] w-[12rem]" />
<Skeleton className="h-[12rem] w-[12rem]" />
<Skeleton className="h-[12rem] w-[12rem]" />
<Skeleton className="h-[12rem] w-[12rem]" />
</div>
</div>
</main>
</div>
);
return <MainMarketplacePageLoading />;
}
// FRONTEND-TODO : Add better Error UI
if (hasError) {
return (
<div className="mx-auto w-screen max-w-[1360px]">
<main className="px-4">
<div className="flex min-h-[400px] items-center justify-center">
<div className="text-lg text-red-500">
Error loading marketplace data. Please try again later.
</div>
<ErrorCard
isSuccess={false}
responseError={{ message: "Failed to load marketplace data" }}
context="marketplace page"
onRetry={() => window.location.reload()}
className="w-full max-w-md"
/>
</div>
</main>
</div>

View File

@@ -0,0 +1,31 @@
import { Skeleton } from "@/components/ui/skeleton";
export const MainMarketplacePageLoading = () => {
return (
<div className="mx-auto w-screen max-w-[1360px]">
<main className="px-4">
<div className="flex flex-col gap-2 pt-16">
<div className="flex flex-col items-center justify-center gap-8">
<Skeleton className="h-16 w-[60%]" />
<Skeleton className="h-12 w-[40%]" />
</div>
<div className="flex flex-col items-center justify-center gap-8 pt-8">
<Skeleton className="h-8 w-[60%]" />
</div>
<div className="mx-auto flex w-[80%] flex-wrap items-center justify-center gap-8 pt-24">
<Skeleton className="h-[12rem] w-[12rem]" />
<Skeleton className="h-[12rem] w-[12rem]" />
<Skeleton className="h-[12rem] w-[12rem]" />
<Skeleton className="h-[12rem] w-[12rem]" />
<Skeleton className="h-[12rem] w-[12rem]" />
<Skeleton className="h-[12rem] w-[12rem]" />
<Skeleton className="h-[12rem] w-[12rem]" />
<Skeleton className="h-[12rem] w-[12rem]" />
<Skeleton className="h-[12rem] w-[12rem]" />
<Skeleton className="h-[12rem] w-[12rem]" />
</div>
</div>
</main>
</div>
);
};

View File

@@ -1,103 +1,55 @@
import BackendAPI from "@/lib/autogpt-server-api";
import { AgentsSection } from "@/components/agptui/composite/AgentsSection";
import { Breadcrumbs } from "@/components/molecules/Breadcrumbs/Breadcrumbs";
import { getQueryClient } from "@/lib/react-query/queryClient";
import {
getV2GetCreatorDetails,
prefetchGetV2GetCreatorDetailsQuery,
prefetchGetV2ListStoreAgentsQuery,
} from "@/app/api/__generated__/endpoints/store/store";
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import { MainCreatorPage } from "../../components/MainCreatorPage/MainCreatorPage";
import { Metadata } from "next";
import { CreatorInfoCard } from "@/components/agptui/CreatorInfoCard";
import { CreatorLinks } from "@/components/agptui/CreatorLinks";
import { Separator } from "@/components/ui/separator";
import { CreatorDetails } from "@/app/api/__generated__/models/creatorDetails";
// Force dynamic rendering to avoid static generation issues with cookies
export const dynamic = "force-dynamic";
type MarketplaceCreatorPageParams = { creator: string };
export interface MarketplaceCreatorPageParams {
creator: string;
}
export async function generateMetadata({
params: _params,
}: {
params: Promise<MarketplaceCreatorPageParams>;
}): Promise<Metadata> {
const api = new BackendAPI();
const params = await _params;
const creator = await api.getStoreCreator(params.creator.toLowerCase());
const { data: creator } = await getV2GetCreatorDetails(
params.creator.toLowerCase(),
);
return {
title: `${creator.name} - AutoGPT Store`,
description: creator.description,
title: `${(creator as CreatorDetails).name} - AutoGPT Store`,
description: (creator as CreatorDetails).description,
};
}
// export async function generateStaticParams() {
// const api = new BackendAPI();
// const creators = await api.getStoreCreators({ featured: true });
// return creators.creators.map((creator) => ({
// creator: creator.username,
// }));
// }
export default async function Page({
params: _params,
}: {
params: Promise<MarketplaceCreatorPageParams>;
}) {
const api = new BackendAPI();
const queryClient = getQueryClient();
const params = await _params;
try {
const creator = await api.getStoreCreator(params.creator);
const creatorAgents = await api.getStoreAgents({ creator: params.creator });
await Promise.all([
prefetchGetV2ListStoreAgentsQuery(queryClient, {
creator: params.creator,
}),
prefetchGetV2GetCreatorDetailsQuery(queryClient, params.creator),
]);
return (
<div className="mx-auto w-screen max-w-[1360px]">
<main className="mt-5 px-4">
<Breadcrumbs
items={[
{ name: "Store", link: "/marketplace" },
{ name: creator.name, link: "#" },
]}
/>
<div className="mt-4 flex flex-col items-start gap-4 sm:mt-6 sm:gap-6 md:mt-8 md:flex-row md:gap-8">
<div className="w-full md:w-auto md:shrink-0">
<CreatorInfoCard
username={creator.name}
handle={creator.username}
avatarSrc={creator.avatar_url}
categories={creator.top_categories}
averageRating={creator.agent_rating}
totalRuns={creator.agent_runs}
/>
</div>
<div className="flex min-w-0 flex-1 flex-col gap-4 sm:gap-6 md:gap-8">
<p className="text-underline-position-from-font text-decoration-skip-none text-left font-poppins text-base font-medium leading-6">
About
</p>
<div
data-testid="creator-description"
className="text-[48px] font-normal leading-[59px] text-neutral-900 dark:text-zinc-50"
style={{ whiteSpace: "pre-line" }}
>
{creator.description}
</div>
<CreatorLinks links={creator.links} />
</div>
</div>
<div className="mt-8 sm:mt-12 md:mt-16 lg:pb-[58px]">
<Separator className="mb-6 bg-gray-200" />
<AgentsSection
agents={creatorAgents.agents}
hideAvatars={true}
sectionTitle={`Agents by ${creator.name}`}
/>
</div>
</main>
</div>
);
} catch {
return (
<div className="flex h-screen w-full items-center justify-center">
<div className="text-2xl text-neutral-900">Creator not found</div>
</div>
);
}
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<MainCreatorPage params={params} />
</HydrationBoundary>
);
}