refactor(frontend): Update data fetching strategy in marketplace main page (#10520)

With this PR, we’re changing the data fetching strategy on the
marketplace page. We’re now using autogenerated React queries.

### Changes

- Splits separate render logic and hook logic.
- Update the data fetching strategy.
- Currently, we’re seeing agents in the featured section and creators in
the featured creators section, even if they’re not set to “isFeatured”
true. I’ve fixed that also.

### 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-01 20:57:48 +05:30
committed by GitHub
parent 8331dabf6a
commit 7705cf243c
22 changed files with 976 additions and 96 deletions

View File

@@ -1,56 +0,0 @@
"use server";
import BackendAPI, {
CreatorsResponse,
StoreAgentsResponse,
} from "@/lib/autogpt-server-api";
const EMPTY_AGENTS_RESPONSE: StoreAgentsResponse = {
agents: [],
pagination: {
total_items: 0,
total_pages: 0,
current_page: 0,
page_size: 0,
},
};
const EMPTY_CREATORS_RESPONSE: CreatorsResponse = {
creators: [],
pagination: {
total_items: 0,
total_pages: 0,
current_page: 0,
page_size: 0,
},
};
export async function getMarketplaceData(): Promise<{
featuredAgents: StoreAgentsResponse;
topAgents: StoreAgentsResponse;
featuredCreators: CreatorsResponse;
}> {
const api = new BackendAPI();
const [featuredAgents, topAgents, featuredCreators] = await Promise.all([
api.getStoreAgents({ featured: true }).catch((error) => {
console.error("Error fetching featured marketplace agents:", error);
return EMPTY_AGENTS_RESPONSE;
}),
api.getStoreAgents({ sorted_by: "runs" }).catch((error) => {
console.error("Error fetching top marketplace agents:", error);
return EMPTY_AGENTS_RESPONSE;
}),
api
.getStoreCreators({ featured: true, sorted_by: "num_agents" })
.catch((error) => {
console.error("Error fetching featured marketplace creators:", error);
return EMPTY_CREATORS_RESPONSE;
}),
]);
return {
featuredAgents,
topAgents,
featuredCreators,
};
}

View File

@@ -0,0 +1,103 @@
"use client";
import {
Carousel,
CarouselContent,
CarouselItem,
} from "@/components/ui/carousel";
import { useAgentsSection } from "./useAgentsSection";
import { StoreAgent } from "@/app/api/__generated__/models/storeAgent";
import { StoreCard } from "../StoreCard/StoreCard";
export interface Agent {
slug: string;
agent_name: string;
agent_image: string;
creator: string;
creator_avatar: string;
sub_heading: string;
description: string;
runs: number;
rating: number;
}
interface AgentsSectionProps {
sectionTitle: string;
agents: StoreAgent[];
hideAvatars?: boolean;
margin?: string;
}
export const AgentsSection = ({
sectionTitle,
agents: allAgents,
hideAvatars = false,
margin = "24px",
}: AgentsSectionProps) => {
// TODO: Update this when we have pagination and shifts to useAgentsSection
const displayedAgents = allAgents;
const { handleCardClick } = useAgentsSection();
return (
<div className="flex flex-col items-center justify-center">
<div className="w-full max-w-[1360px]">
<h2
style={{ marginBottom: margin }}
className="font-poppins text-lg font-semibold text-[#282828] dark:text-neutral-200"
>
{sectionTitle}
</h2>
{!displayedAgents || displayedAgents.length === 0 ? (
<div className="text-center text-gray-500 dark:text-gray-400">
No agents found
</div>
) : (
<>
{/* Mobile Carousel View */}
<Carousel
className="md:hidden"
opts={{
loop: true,
}}
>
<CarouselContent>
{displayedAgents.map((agent, index) => (
<CarouselItem key={index} className="min-w-64 max-w-71">
<StoreCard
agentName={agent.agent_name}
agentImage={agent.agent_image}
description={agent.description}
runs={agent.runs}
rating={agent.rating}
avatarSrc={agent.creator_avatar}
creatorName={agent.creator}
hideAvatar={hideAvatars}
onClick={() => handleCardClick(agent.creator, agent.slug)}
/>
</CarouselItem>
))}
</CarouselContent>
</Carousel>
<div className="hidden grid-cols-1 place-items-center gap-6 md:grid md:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
{displayedAgents.map((agent, index) => (
<StoreCard
key={index}
agentName={agent.agent_name}
agentImage={agent.agent_image}
description={agent.description}
runs={agent.runs}
rating={agent.rating}
avatarSrc={agent.creator_avatar}
creatorName={agent.creator}
hideAvatar={hideAvatars}
onClick={() => handleCardClick(agent.creator, agent.slug)}
/>
))}
</div>
</>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,14 @@
import { useRouter } from "next/navigation";
// FRONTEND-TODO : Need to add more logic for pagination in future
export const useAgentsSection = () => {
const router = useRouter();
const handleCardClick = (creator: string, slug: string) => {
router.push(
`/marketplace/agent/${encodeURIComponent(creator)}/${encodeURIComponent(slug)}`,
);
};
return { handleCardClick };
};

View File

@@ -0,0 +1,51 @@
"use client";
import { PublishAgentModal } from "@/components/contextual/PublishAgentModal/PublishAgentModal";
import * as React from "react";
interface BecomeACreatorProps {
title?: string;
description?: string;
buttonText?: string;
}
export function BecomeACreator({
title = "Become a creator",
description = "Join a community where your AI creations can inspire, engage, and be downloaded by users around the world.",
buttonText = "Upload your agent",
}: BecomeACreatorProps) {
return (
<div className="relative mx-auto h-auto min-h-[300px] w-full max-w-[1360px] md:min-h-[400px] lg:h-[459px]">
{/* Title */}
<h2 className="mb-[77px] font-poppins text-[18px] font-semibold leading-[28px] text-neutral-800 dark:text-neutral-200">
{title}
</h2>
{/* Content Container */}
<div className="mx-auto w-full max-w-[900px] px-4 text-center md:px-6 lg:px-0">
<h2 className="mb-6 text-center font-poppins text-[48px] font-semibold leading-[54px] tracking-[-0.012em] text-neutral-950 dark:text-neutral-50 md:mb-8 lg:mb-12">
Build AI agents and share
<br />
<span className="text-violet-600 dark:text-violet-400">
your
</span>{" "}
vision
</h2>
<p className="mx-auto mb-8 max-w-[90%] text-lg font-normal leading-relaxed text-neutral-700 dark:text-neutral-300 md:mb-10 md:text-xl md:leading-loose lg:mb-14 lg:text-2xl">
{description}
</p>
<PublishAgentModal
trigger={
<button className="inline-flex h-[48px] cursor-pointer items-center justify-center rounded-[38px] bg-neutral-800 px-8 py-3 transition-colors hover:bg-neutral-700 dark:bg-neutral-700 dark:hover:bg-neutral-600 md:h-[56px] md:px-10 md:py-4 lg:h-[68px] lg:px-12 lg:py-5">
<span className="whitespace-nowrap font-poppins text-base font-medium leading-normal text-neutral-50 md:text-lg md:leading-relaxed lg:text-xl lg:leading-7">
{buttonText}
</span>
</button>
}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,57 @@
import Image from "next/image";
import { backgroundColor } from "./helper";
interface CreatorCardProps {
creatorName: string;
creatorImage: string;
bio: string;
agentsUploaded: number;
onClick: () => void;
index: number;
}
export const CreatorCard = ({
creatorName,
creatorImage,
bio,
agentsUploaded,
onClick,
index,
}: CreatorCardProps) => {
return (
<div
className={`h-[264px] w-full px-[18px] pb-5 pt-6 ${backgroundColor(index)} inline-flex cursor-pointer flex-col items-start justify-start gap-3.5 rounded-[26px] transition-all duration-200 hover:brightness-95`}
onClick={onClick}
data-testid="creator-card"
>
<div className="relative h-[64px] w-[64px]">
<div className="absolute inset-0 overflow-hidden rounded-full">
{creatorImage ? (
<Image
src={creatorImage}
alt={creatorName}
width={64}
height={64}
className="h-full w-full object-cover"
priority
/>
) : (
<div className="h-full w-full bg-neutral-300 dark:bg-neutral-600" />
)}
</div>
</div>
<div className="flex flex-col gap-2">
<h3 className="font-poppins text-2xl font-semibold leading-tight text-neutral-900 dark:text-neutral-100">
{creatorName}
</h3>
<p className="text-sm font-normal leading-normal text-neutral-600 dark:text-neutral-400">
{bio}
</p>
<div className="text-lg font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
{agentsUploaded} agents
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,8 @@
const BACKGROUND_COLORS = [
"bg-amber-100 dark:bg-amber-800", // #fef3c7 / #92400e
"bg-violet-100 dark:bg-violet-800", // #ede9fe / #5b21b6
"bg-green-100 dark:bg-green-800", // #dcfce7 / #065f46
"bg-blue-100 dark:bg-blue-800", // #dbeafe / #1e3a8a
];
export const backgroundColor = (index: number) =>
BACKGROUND_COLORS[index % BACKGROUND_COLORS.length];

View File

@@ -0,0 +1,74 @@
import Image from "next/image";
import { StarRatingIcons } from "@/components/ui/icons";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useState } from "react";
import { StoreAgent } from "@/app/api/__generated__/models/storeAgent";
interface FeaturedStoreCardProps {
agent: StoreAgent;
backgroundColor: string;
}
export const FeaturedAgentCard = ({
agent,
backgroundColor,
}: FeaturedStoreCardProps) => {
// TODO: Need to use group for hover
const [isHovered, setIsHovered] = useState(false);
return (
<Card
data-testid="featured-store-card"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={`flex h-full flex-col ${backgroundColor} rounded-[1.5rem] border-none`}
>
<CardHeader>
<CardTitle className="line-clamp-2 text-base sm:text-xl">
{agent.agent_name}
</CardTitle>
<CardDescription className="text-sm">
By {agent.creator}
</CardDescription>
</CardHeader>
<CardContent className="flex-1 p-4">
<div className="relative aspect-[4/3] w-full overflow-hidden rounded-xl">
<Image
src={agent.agent_image || "/autogpt-logo-dark-bg.png"}
alt={`${agent.agent_name} preview`}
fill
sizes="100%"
className={`object-cover transition-opacity duration-200 ${
isHovered ? "opacity-0" : "opacity-100"
}`}
/>
<div
className={`absolute inset-0 overflow-y-auto p-4 transition-opacity duration-200 ${
isHovered ? "opacity-100" : "opacity-0"
}`}
>
<CardDescription className="line-clamp-[6] text-xs sm:line-clamp-[8] sm:text-sm">
{agent.description}
</CardDescription>
</div>
</div>
</CardContent>
<CardFooter className="flex items-center justify-between">
<div className="font-semibold">
{agent.runs?.toLocaleString() ?? "0"} runs
</div>
<div className="flex items-center gap-1.5">
<p>{agent.rating.toFixed(1) ?? "0.0"}</p>
{StarRatingIcons(agent.rating)}
</div>
</CardFooter>
</Card>
);
};

View File

@@ -0,0 +1,42 @@
"use client";
import { CreatorCard } from "../CreatorCard/CreatorCard";
import { useFeaturedCreators } from "./useFeaturedCreators";
import { Creator } from "@/app/api/__generated__/models/creator";
interface FeaturedCreatorsProps {
title?: string;
featuredCreators: Creator[];
}
export const FeaturedCreators = ({
featuredCreators,
title = "Featured Creators",
}: FeaturedCreatorsProps) => {
const { handleCardClick, displayedCreators } = useFeaturedCreators({
featuredCreators,
});
return (
<div className="flex w-full flex-col items-center justify-center">
<div className="w-full max-w-[1360px]">
<h2 className="mb-9 font-poppins text-lg font-semibold text-neutral-800 dark:text-neutral-200">
{title}
</h2>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
{displayedCreators.map((creator, index) => (
<CreatorCard
key={index}
creatorName={creator.name || creator.username}
creatorImage={creator.avatar_url}
bio={creator.description}
agentsUploaded={creator.num_agents}
onClick={() => handleCardClick(creator.username)}
index={index}
/>
))}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,23 @@
import { Creator } from "@/app/api/__generated__/models/creator";
import { useRouter } from "next/navigation";
interface useFeaturedCreatorsProps {
featuredCreators: Creator[];
}
export const useFeaturedCreators = ({
featuredCreators,
}: useFeaturedCreatorsProps) => {
const router = useRouter();
const handleCardClick = (creator: string) => {
router.push(`/marketplace/creator/${encodeURIComponent(creator)}`);
};
const displayedCreators = featuredCreators.slice(0, 4);
return {
handleCardClick,
displayedCreators,
};
};

View File

@@ -0,0 +1,64 @@
"use client";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
CarouselIndicator,
} from "@/components/ui/carousel";
import Link from "next/link";
import { useFeaturedSection } from "./useFeaturedSection";
import { StoreAgent } from "@/app/api/__generated__/models/storeAgent";
import { getBackgroundColor } from "./helper";
import { FeaturedAgentCard } from "../FeaturedAgentCard/FeaturedAgentCard";
interface FeaturedSectionProps {
featuredAgents: StoreAgent[];
}
export const FeaturedSection = ({ featuredAgents }: FeaturedSectionProps) => {
const { handleNextSlide, handlePrevSlide } = useFeaturedSection({
featuredAgents,
});
return (
<section className="w-full">
<h2 className="mb-8 font-poppins text-2xl font-semibold leading-7 text-neutral-800 dark:text-neutral-200">
Featured agents
</h2>
<Carousel
opts={{
align: "center",
containScroll: "trimSnaps",
}}
>
<CarouselContent>
{featuredAgents.map((agent, index) => (
<CarouselItem
key={index}
className="h-[480px] md:basis-1/2 lg:basis-1/3"
>
<Link
href={`/marketplace/agent/${encodeURIComponent(agent.creator)}/${encodeURIComponent(agent.slug)}`}
className="block h-full"
>
<FeaturedAgentCard
agent={agent}
backgroundColor={getBackgroundColor(index)}
/>
</Link>
</CarouselItem>
))}
</CarouselContent>
<div className="relative mt-4">
<CarouselIndicator />
<CarouselPrevious afterClick={handlePrevSlide} />
<CarouselNext afterClick={handleNextSlide} />
</div>
</Carousel>
</section>
);
};

View File

@@ -0,0 +1,9 @@
const BACKGROUND_COLORS = [
"bg-violet-200 dark:bg-violet-800", // #ddd6fe / #5b21b6
"bg-blue-200 dark:bg-blue-800", // #bfdbfe / #1e3a8a
"bg-green-200 dark:bg-green-800", // #bbf7d0 / #065f46
];
export const getBackgroundColor = (index: number) => {
return BACKGROUND_COLORS[index % BACKGROUND_COLORS.length];
};

View File

@@ -0,0 +1,29 @@
import { useState } from "react";
import { StoreAgent } from "@/app/api/__generated__/models/storeAgent";
interface useFeaturedSectionProps {
featuredAgents: StoreAgent[];
}
export const useFeaturedSection = ({
featuredAgents,
}: useFeaturedSectionProps) => {
const [_, setCurrentSlide] = useState(0);
const handlePrevSlide = () => {
setCurrentSlide((prev) =>
prev === 0 ? featuredAgents.length - 1 : prev - 1,
);
};
const handleNextSlide = () => {
setCurrentSlide((prev) =>
prev === featuredAgents.length - 1 ? 0 : prev + 1,
);
};
return {
handleNextSlide,
handlePrevSlide,
};
};

View File

@@ -0,0 +1,40 @@
"use client";
import { Badge } from "@/components/ui/badge";
import { useFilterChips } from "./useFilterChips";
interface FilterChipsProps {
badges: string[];
onFilterChange?: (selectedFilters: string[]) => void;
multiSelect?: boolean;
}
// Some flaws in its logic
// FRONTEND-TODO : This needs to be fixed
export const FilterChips = ({
badges,
onFilterChange,
multiSelect = true,
}: FilterChipsProps) => {
const { selectedFilters, handleBadgeClick } = useFilterChips({
multiSelect,
onFilterChange,
});
return (
<div className="flex h-auto min-h-8 flex-wrap items-center justify-center gap-3 lg:min-h-14 lg:justify-start lg:gap-5">
{badges.map((badge) => (
<Badge
key={badge}
variant={selectedFilters.includes(badge) ? "secondary" : "outline"}
className="mb-2 flex cursor-pointer items-center justify-center gap-2 rounded-full border border-black/50 px-3 py-1 dark:border-white/50 lg:mb-3 lg:gap-2.5 lg:px-6 lg:py-2"
onClick={() => handleBadgeClick(badge)}
>
<div className="text-sm font-light tracking-tight text-[#474747] dark:text-[#e0e0e0] lg:text-xl lg:font-medium lg:leading-9">
{badge}
</div>
</Badge>
))}
</div>
);
};

View File

@@ -0,0 +1,37 @@
import { useState } from "react";
interface useFilterChipsProps {
onFilterChange?: (selectedFilters: string[]) => void;
multiSelect?: boolean;
}
export const useFilterChips = ({
onFilterChange,
multiSelect,
}: useFilterChipsProps) => {
const [selectedFilters, setSelectedFilters] = useState<string[]>([]);
const handleBadgeClick = (badge: string) => {
setSelectedFilters((prevFilters) => {
let newFilters;
if (multiSelect) {
newFilters = prevFilters.includes(badge)
? prevFilters.filter((filter) => filter !== badge)
: [...prevFilters, badge];
} else {
newFilters = prevFilters.includes(badge) ? [] : [badge];
}
if (onFilterChange) {
onFilterChange(newFilters);
}
return newFilters;
});
};
return {
selectedFilters,
handleBadgeClick,
};
};

View File

@@ -0,0 +1,53 @@
"use client";
import { FilterChips } from "../FilterChips/FilterChips";
import { SearchBar } from "../SearchBar/SearchBar";
import { useHeroSection } from "./useHeroSection";
export const HeroSection = () => {
const { onFilterChange } = useHeroSection();
return (
<div className="mb-2 mt-8 flex flex-col items-center justify-center px-4 sm:mb-4 sm:mt-12 sm:px-6 md:mb-6 md:mt-16 lg:my-24 lg:px-8 xl:my-16">
<div className="w-full max-w-3xl lg:max-w-4xl xl:max-w-5xl">
<div className="mb-4 text-center md:mb-8">
<h1 className="text-center">
<span className="font-poppins text-[48px] font-semibold leading-[54px] text-neutral-950 dark:text-neutral-50">
Explore AI agents built for{" "}
</span>
<span className="font-poppins text-[48px] font-semibold leading-[54px] text-violet-600">
you
</span>
<br />
<span className="font-poppins text-[48px] font-semibold leading-[54px] text-neutral-950 dark:text-neutral-50">
by the{" "}
</span>
<span className="font-poppins text-[48px] font-semibold leading-[54px] text-blue-500">
community
</span>
</h1>
</div>
<h3 className="mb:text-2xl mb-6 text-center font-sans text-xl font-normal leading-loose text-neutral-700 dark:text-neutral-300 md:mb-12">
Bringing you AI agents designed by thinkers from around the world
</h3>
<div className="mb-4 flex justify-center sm:mb-5">
<SearchBar height="h-[74px]" />
</div>
<div>
<div className="flex justify-center">
<FilterChips
badges={[
"Marketing",
"SEO",
"Content Creation",
"Automation",
"Fun",
]}
onFilterChange={onFilterChange}
multiSelect={false}
/>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,22 @@
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export const useHeroSection = () => {
const router = useRouter();
const { completeStep } = useOnboarding();
// Mark marketplace visit task as completed
useEffect(() => {
completeStep("MARKETPLACE_VISIT");
}, [completeStep]);
function onFilterChange(selectedFilters: string[]) {
const encodedTerm = encodeURIComponent(selectedFilters.join(", "));
router.push(`/marketplace/search?searchTerm=${encodedTerm}`);
}
return {
onFilterChange,
};
};

View File

@@ -0,0 +1,69 @@
"use client";
import { Separator } from "@/components/ui/separator";
import { FeaturedSection } from "../FeaturedSection/FeaturedSection";
import { BecomeACreator } from "../BecomeACreator/BecomeACreator";
import { HeroSection } from "../HeroSection/HeroSection";
import { AgentsSection } from "../AgentsSection/AgentsSection";
import { useMainMarketplacePage } from "./useMainMarketplacePage";
import { FeaturedCreators } from "../FeaturedCreators/FeaturedCreators";
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 min-h-[400px] items-center justify-center">
<div className="text-lg">Loading...</div>
</div>
</main>
</div>
);
}
// 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>
</div>
</main>
</div>
);
}
return (
// FRONTEND-TODO : Need better state location, need to fetch creators and agents in their respective file, Can't do it right now because these files are used in some other pages of marketplace, will fix it when encounter with those pages
<div className="mx-auto w-screen max-w-[1360px]">
<main className="px-4">
<HeroSection />
{featuredAgents && (
<FeaturedSection featuredAgents={featuredAgents.agents} />
)}
{/* 100px margin because our featured sections button are placed 40px below the container */}
<Separator className="mb-6 mt-24" />
{topAgents && (
<AgentsSection sectionTitle="Top Agents" agents={topAgents.agents} />
)}
<Separator className="mb-[25px] mt-[60px]" />
{featuredCreators && (
<FeaturedCreators featuredCreators={featuredCreators.creators} />
)}
<Separator className="mb-[25px] mt-[60px]" />
<BecomeACreator
title="Become a Creator"
description="Join our ever-growing community of hackers and tinkerers"
buttonText="Become a Creator"
/>
</main>
</div>
);
};

View File

@@ -0,0 +1,69 @@
import {
useGetV2ListStoreAgents,
useGetV2ListStoreCreators,
} from "@/app/api/__generated__/endpoints/store/store";
import { StoreAgentsResponse } from "@/app/api/__generated__/models/storeAgentsResponse";
import { CreatorsResponse } from "@/app/api/__generated__/models/creatorsResponse";
export const useMainMarketplacePage = () => {
// Below queries are already fetched on server and hydrated properly in cache, hence these requests are fast
const {
data: featuredAgents,
isLoading: isFeaturedAgentsLoading,
isError: isFeaturedAgentsError,
} = useGetV2ListStoreAgents(
{ featured: true },
{
query: {
select: (x) => {
return x.data as StoreAgentsResponse;
},
},
},
);
const {
data: topAgents,
isLoading: isTopAgentsLoading,
isError: isTopAgentsError,
} = useGetV2ListStoreAgents(
{
sorted_by: "runs",
},
{
query: {
select: (x) => {
return x.data as StoreAgentsResponse;
},
},
},
);
const {
data: featuredCreators,
isLoading: isFeaturedCreatorsLoading,
isError: isFeaturedCreatorsError,
} = useGetV2ListStoreCreators(
{ featured: true, sorted_by: "num_agents" },
{
query: {
select: (x) => {
return x.data as CreatorsResponse;
},
},
},
);
const isLoading =
isFeaturedAgentsLoading || isTopAgentsLoading || isFeaturedCreatorsLoading;
const hasError =
isFeaturedAgentsError || isTopAgentsError || isFeaturedCreatorsError;
return {
featuredAgents,
topAgents,
featuredCreators,
isLoading,
hasError,
};
};

View File

@@ -0,0 +1,44 @@
"use client";
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
import { useSearchbar } from "./useSearchBar";
interface SearchBarProps {
placeholder?: string;
backgroundColor?: string;
iconColor?: string;
textColor?: string;
placeholderColor?: string;
width?: string;
height?: string;
}
export const SearchBar = ({
placeholder = 'Search for tasks like "optimise SEO"',
backgroundColor = "bg-neutral-100 dark:bg-neutral-800",
iconColor = "text-[#646464] dark:text-neutral-400",
textColor = "text-[#707070] dark:text-neutral-200",
placeholderColor = "text-[#707070] dark:text-neutral-400",
width = "w-9/10 lg:w-[56.25rem]",
height = "h-[60px]",
}: SearchBarProps) => {
const { handleSubmit, setSearchQuery, searchQuery } = useSearchbar();
return (
<form
onSubmit={handleSubmit}
data-testid="store-search-bar"
className={`${width} ${height} px-4 pt-2 md:px-6 md:pt-1 ${backgroundColor} flex items-center justify-center gap-2 rounded-full md:gap-5`}
>
<MagnifyingGlassIcon className={`h-5 w-5 md:h-7 md:w-7 ${iconColor}`} />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={placeholder}
className={`flex-grow border-none bg-transparent ${textColor} font-sans text-lg font-normal leading-[2.25rem] tracking-tight md:text-xl placeholder:${placeholderColor} focus:outline-none`}
data-testid="store-search-input"
/>
</form>
);
};

View File

@@ -0,0 +1,24 @@
import { useRouter } from "next/navigation";
import { useState } from "react";
export const useSearchbar = () => {
const router = useRouter();
const [searchQuery, setSearchQuery] = useState("");
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log(searchQuery);
if (searchQuery.trim()) {
const encodedTerm = encodeURIComponent(searchQuery);
router.push(`/marketplace/search?searchTerm=${encodedTerm}`);
}
};
return {
handleSubmit,
setSearchQuery,
searchQuery,
};
};

View File

@@ -0,0 +1,120 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import Image from "next/image";
import { StarRatingIcons } from "@/components/ui/icons";
interface StoreCardProps {
agentName: string;
agentImage: string;
description: string;
runs: number;
rating: number;
onClick: () => void;
avatarSrc: string;
hideAvatar?: boolean;
creatorName?: string;
}
export const StoreCard: React.FC<StoreCardProps> = ({
agentName,
agentImage,
description,
runs,
rating,
onClick,
avatarSrc,
hideAvatar = false,
creatorName,
}) => {
const handleClick = () => {
onClick();
};
return (
<div
className="flex h-[27rem] w-full max-w-md cursor-pointer flex-col items-start rounded-3xl bg-background transition-all duration-300 hover:shadow-lg dark:hover:shadow-gray-700"
onClick={handleClick}
data-testid="store-card"
role="button"
tabIndex={0}
aria-label={`${agentName} agent card`}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleClick();
}
}}
>
{/* First Section: Image with Avatar */}
<div className="relative aspect-[2/1.2] w-full overflow-hidden rounded-3xl md:aspect-[2.17/1]">
{agentImage && (
<Image
src={agentImage}
alt={`${agentName} preview image`}
fill
className="object-cover"
priority
/>
)}
{!hideAvatar && (
<div className="absolute bottom-4 left-4">
<Avatar className="h-16 w-16">
{avatarSrc && (
<AvatarImage
src={avatarSrc}
alt={`${creatorName || agentName} creator avatar`}
/>
)}
<AvatarFallback size={64}>
{(creatorName || agentName).charAt(0)}
</AvatarFallback>
</Avatar>
</div>
)}
</div>
<div className="mt-3 flex w-full flex-1 flex-col px-4">
{/* Second Section: Agent Name and Creator Name */}
<div className="flex w-full flex-col">
<h3 className="line-clamp-2 font-poppins text-2xl font-semibold text-[#272727] dark:text-neutral-100">
{agentName}
</h3>
{!hideAvatar && creatorName && (
<p className="mt-3 truncate font-sans text-xl font-normal text-neutral-600 dark:text-neutral-400">
by {creatorName}
</p>
)}
</div>
{/* Third Section: Description */}
<div className="mt-2.5 flex w-full flex-col">
<p className="line-clamp-3 text-base font-normal leading-normal text-neutral-600 dark:text-neutral-400">
{description}
</p>
</div>
<div className="flex-grow" />
{/* Spacer to push stats to bottom */}
{/* Fourth Section: Stats Row - aligned to bottom */}
<div className="mt-5 w-full">
<div className="flex items-center justify-between">
<div className="text-lg font-semibold text-neutral-800 dark:text-neutral-200">
{runs.toLocaleString()} runs
</div>
<div className="flex items-center gap-2">
<span className="text-lg font-semibold text-neutral-800 dark:text-neutral-200">
{rating.toFixed(1)}
</span>
<div
className="inline-flex items-center"
role="img"
aria-label={`Rating: ${rating.toFixed(1)} out of 5 stars`}
>
{StarRatingIcons(rating)}
</div>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@@ -1,21 +1,12 @@
import React from "react";
import { HeroSection } from "@/components/agptui/composite/HeroSection";
import { FeaturedSection } from "@/components/agptui/composite/FeaturedSection";
import {
AgentsSection,
Agent,
} from "@/components/agptui/composite/AgentsSection";
import { BecomeACreator } from "@/components/agptui/BecomeACreator";
import {
FeaturedCreators,
FeaturedCreator,
} from "@/components/agptui/composite/FeaturedCreators";
import { Separator } from "@/components/ui/separator";
import { Metadata } from "next";
import {
prefetchGetV2ListStoreAgentsQuery,
prefetchGetV2ListStoreCreatorsQuery,
} from "@/app/api/__generated__/endpoints/store/store";
import { getQueryClient } from "@/lib/react-query/queryClient";
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import { MainMarkeplacePage } from "./components/MainMarketplacePage/MainMarketplacePage";
import { getMarketplaceData } from "./actions";
// Force dynamic rendering to avoid static generation issues with cookies
export const dynamic = "force-dynamic";
// FIX: Correct metadata
@@ -63,31 +54,24 @@ export const metadata: Metadata = {
};
export default async function MarketplacePage(): Promise<React.ReactElement> {
const { featuredAgents, topAgents, featuredCreators } =
await getMarketplaceData();
const queryClient = getQueryClient();
await Promise.all([
prefetchGetV2ListStoreAgentsQuery(queryClient, {
featured: true,
}),
prefetchGetV2ListStoreAgentsQuery(queryClient, {
sorted_by: "runs",
}),
prefetchGetV2ListStoreCreatorsQuery(queryClient, {
featured: true,
sorted_by: "num_agents",
}),
]);
return (
<div className="mx-auto w-screen max-w-[1360px]">
<main className="px-4">
<HeroSection />
<FeaturedSection featuredAgents={featuredAgents.agents} />
{/* 100px margin because our featured sections button are placed 40px below the container */}
<Separator className="mb-6 mt-24" />
<AgentsSection
sectionTitle="Top Agents"
agents={topAgents.agents as Agent[]}
/>
<Separator className="mb-[25px] mt-[60px]" />
<FeaturedCreators
featuredCreators={featuredCreators.creators as FeaturedCreator[]}
/>
<Separator className="mb-[25px] mt-[60px]" />
<BecomeACreator
title="Become a Creator"
description="Join our ever-growing community of hackers and tinkerers"
buttonText="Become a Creator"
/>
</main>
</div>
<HydrationBoundary state={dehydrate(queryClient)}>
<MainMarkeplacePage />
</HydrationBoundary>
);
}