refactor(frontend): simplify marketplace search page and update data fetching (#11061)

This PR refactors the marketplace search page to improve code
maintainability, readability, and follows modern React patterns by
extracting complex logic into a custom hook and creating dedicated
components.

### 🔄 Changes

#### **Architecture Improvements**
- **Component Extraction**: Replaced the monolithic `SearchResults`
component with a cleaner `MainSearchResultPage` component that focuses
solely on presentation
- **Custom Hook Pattern**: Extracted all business logic and state
management into `useMainSearchResultPage` hook for better separation of
concerns
- **Loading State Component**: Added dedicated
`MainSearchResultPageLoading` component for consistent loading UI

#### **Code Simplification**
- **Reduced search page to 19 lines** (from 175 lines) by removing
inline logic and state management
- **Centralized data fetching** using auto-generated API endpoints
(`useGetV2ListStoreAgents`, `useGetV2ListStoreCreators`)
- **Improved error handling** with dedicated error states and loading
states

#### **Feature Updates**
- **Sort Options**: Commented out "Most Recent" and "Highest Rated" sort
options due to backend limitations (no date/rating data available)
- **Client-side Sorting**: Implemented client-side sorting for "runs"
and "rating" as a temporary solution
- **Search Filters**: Maintained filter functionality for
agents/creators with improved state management

### 📊 Impact

- **Better Developer Experience**: Code is now more modular and easier
to understand
- **Improved Maintainability**: Business logic separated from
presentation layer
- **Future-Ready**: Structure prepared for backend improvements when
date/rating data becomes available
- **Type Safety**: Leveraging TypeScript with auto-generated API types

### 🧪 Testing Checklist

- [x] Search functionality works correctly with various search terms
- [x] Filter chips correctly toggle between "All", "Agents", and
"Creators"
- [x] Sort dropdown displays only "Most Runs" option
- [x] Client-side sorting correctly sorts agents and creators by runs
- [x] Loading state displays while fetching data
- [x] Error state displays when API calls fail
- [x] "No results found" message appears for empty searches
- [x] Search bar in results page is functional
- [x] Results display correctly with proper layout and styling
This commit is contained in:
Abhimanyu Yadav
2025-10-06 18:23:45 +05:30
committed by GitHub
parent c42f94ce2a
commit a7306970b8
6 changed files with 269 additions and 175 deletions

View File

@@ -0,0 +1,113 @@
import { SearchBar } from "@/components/__legacy__/SearchBar";
import { useMainSearchResultPage } from "./useMainSearchResultPage";
import { SearchFilterChips } from "@/components/__legacy__/SearchFilterChips";
import { SortDropdown } from "@/components/__legacy__/SortDropdown";
import { AgentsSection } from "../AgentsSection/AgentsSection";
import { Separator } from "@/components/__legacy__/ui/separator";
import { FeaturedCreators } from "../FeaturedCreators/FeaturedCreators";
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
import { MainMarketplacePageLoading } from "../MainMarketplacePageLoading";
export const MainSearchResultPage = ({
searchTerm,
sort,
}: {
searchTerm: string;
sort: string;
}) => {
const {
agents,
creators,
totalCount,
agentsCount,
creatorsCount,
handleFilterChange,
handleSortChange,
showAgents,
showCreators,
isAgentsLoading,
isCreatorsLoading,
isAgentsError,
isCreatorsError,
} = useMainSearchResultPage({ searchTerm, sort });
const isLoading = isAgentsLoading || isCreatorsLoading;
const hasError = isAgentsError || isCreatorsError;
if (isLoading) {
return <MainMarketplacePageLoading />;
}
if (hasError) {
return (
<div className="flex min-h-[500px] items-center justify-center">
<ErrorCard
isSuccess={false}
responseError={{ message: "Failed to load marketplace data" }}
context="marketplace page"
onRetry={() => window.location.reload()}
/>
</div>
);
}
return (
<div className="w-full">
<div className="mx-auto min-h-screen max-w-[1440px] px-10 lg:min-w-[1440px]">
<div className="mt-8 flex items-center">
<div className="flex-1">
<h2 className="text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
Results for:
</h2>
<h1 className="font-poppins text-2xl font-semibold leading-[32px] text-neutral-800 dark:text-neutral-100">
{searchTerm}
</h1>
</div>
<div className="flex-none">
<SearchBar width="w-[439px]" height="h-[60px]" />
</div>
</div>
{totalCount > 0 ? (
<>
<div className="mt-[36px] flex items-center justify-between">
<SearchFilterChips
totalCount={totalCount}
agentsCount={agentsCount}
creatorsCount={creatorsCount}
onFilterChange={handleFilterChange}
/>
<SortDropdown onSort={handleSortChange} />
</div>
{/* Content section */}
<div className="min-h-[500px] max-w-[1440px] space-y-8 py-8">
{showAgents && agentsCount > 0 && agents && (
<div className="mt-[36px]">
<AgentsSection agents={agents} sectionTitle="Agents" />
</div>
)}
{showAgents && agentsCount > 0 && creatorsCount > 0 && (
<Separator />
)}
{showCreators && creatorsCount > 0 && creators && (
<FeaturedCreators
featuredCreators={creators}
title="Creators"
/>
)}
</div>
</>
) : (
<div className="mt-20 flex flex-col items-center justify-center">
<h3 className="mb-2 text-xl font-medium text-neutral-600 dark:text-neutral-300">
No results found
</h3>
<p className="text-neutral-500 dark:text-neutral-400">
Try adjusting your search terms or filters
</p>
</div>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,123 @@
import {
useGetV2ListStoreAgents,
useGetV2ListStoreCreators,
} from "@/app/api/__generated__/endpoints/store/store";
import { CreatorsResponse } from "@/app/api/__generated__/models/creatorsResponse";
import { StoreAgentsResponse } from "@/app/api/__generated__/models/storeAgentsResponse";
import { useState, useMemo } from "react";
interface useMainSearchResultPageType {
searchTerm: string;
sort: string;
}
export const useMainSearchResultPage = ({
searchTerm,
sort,
}: useMainSearchResultPageType) => {
const [showAgents, setShowAgents] = useState(true);
const [showCreators, setShowCreators] = useState(true);
const [clientSortBy, setClientSortBy] = useState<string>(sort);
const {
data: agentsData,
isLoading: isAgentsLoading,
isError: isAgentsError,
} = useGetV2ListStoreAgents(
{
search_query: searchTerm,
sorted_by: sort,
},
{
query: {
select: (x) => {
return (x.data as StoreAgentsResponse).agents;
},
},
},
);
const {
data: creatorsData,
isLoading: isCreatorsLoading,
isError: isCreatorsError,
} = useGetV2ListStoreCreators(
{ search_query: searchTerm, sorted_by: sort },
{
query: {
select: (x) => {
return (x.data as CreatorsResponse).creators;
},
},
},
);
// This is the strategy, we are using for sorting the agents and creators.
// currently we are doing it client side but maybe we will shift it to the server side.
// we will store the sortBy state in the url params, and then refetch the data with the new sortBy.
const agents = useMemo(() => {
if (!agentsData) return [];
const sorted = [...agentsData];
if (clientSortBy === "runs") {
return sorted.sort((a, b) => b.runs - a.runs);
} else if (clientSortBy === "rating") {
return sorted.sort((a, b) => b.rating - a.rating);
} else {
return sorted;
}
}, [agentsData, clientSortBy]);
const creators = useMemo(() => {
if (!creatorsData) return [];
const sorted = [...creatorsData];
if (clientSortBy === "runs") {
return sorted.sort((a, b) => b.agent_runs - a.agent_runs);
} else if (clientSortBy === "rating") {
return sorted.sort((a, b) => b.agent_rating - a.agent_rating);
} else {
return sorted.sort((a, b) => b.num_agents - a.num_agents);
}
}, [creatorsData, clientSortBy]);
const agentsCount = agents?.length ?? 0;
const creatorsCount = creators?.length ?? 0;
const totalCount = agentsCount + creatorsCount;
const handleFilterChange = (value: string) => {
if (value === "agents") {
setShowAgents(true);
setShowCreators(false);
} else if (value === "creators") {
setShowAgents(false);
setShowCreators(true);
} else {
setShowAgents(true);
setShowCreators(true);
}
};
const handleSortChange = (sortValue: string) => {
setClientSortBy(sortValue);
};
return {
agents,
creators,
handleFilterChange,
handleSortChange,
agentsCount,
creatorsCount,
totalCount,
showAgents,
showCreators,
isAgentsLoading,
isCreatorsLoading,
isAgentsError,
isCreatorsError,
};
};

View File

@@ -0,0 +1,27 @@
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
export const MainSearchResultPageLoading = () => {
return (
<div className="w-full">
<div className="mx-auto min-h-screen max-w-[1440px] px-10 lg:min-w-[1440px]">
<div className="mt-8 flex items-center">
<div className="flex-1">
<Skeleton className="mb-2 h-5 w-32 bg-neutral-200 dark:bg-neutral-700" />
<Skeleton className="h-8 w-64 bg-neutral-200 dark:bg-neutral-700" />
</div>
<div className="flex-none">
<Skeleton className="h-[60px] w-[439px] bg-neutral-200 dark:bg-neutral-700" />
</div>
</div>
<div className="mt-[36px] flex items-center justify-between">
<Skeleton className="h-8 w-48 bg-neutral-200 dark:bg-neutral-700" />
<Skeleton className="h-8 w-32 bg-neutral-200 dark:bg-neutral-700" />
</div>
<div className="mt-20 flex flex-col items-center justify-center">
<Skeleton className="mb-4 h-6 w-40 bg-neutral-200 dark:bg-neutral-700" />
<Skeleton className="h-6 w-80 bg-neutral-200 dark:bg-neutral-700" />
</div>
</div>
</div>
);
};

View File

@@ -1,14 +1,7 @@
"use client";
import { use, useCallback, useEffect, useState } from "react";
import { AgentsSection } from "@/components/__legacy__/composite/AgentsSection";
import { SearchBar } from "@/components/__legacy__/SearchBar";
import { FeaturedCreators } from "@/components/__legacy__/composite/FeaturedCreators";
import { Separator } from "@/components/__legacy__/ui/separator";
import { SearchFilterChips } from "@/components/__legacy__/SearchFilterChips";
import { SortDropdown } from "@/components/__legacy__/SortDropdown";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { Creator, StoreAgent } from "@/lib/autogpt-server-api";
import { use } from "react";
import { MainSearchResultPage } from "../components/MainSearchResultPage/MainSearchResultPage";
type MarketplaceSearchPageSearchParams = { searchTerm?: string; sort?: string };
@@ -18,171 +11,9 @@ export default function MarketplaceSearchPage({
searchParams: Promise<MarketplaceSearchPageSearchParams>;
}) {
return (
<SearchResults
<MainSearchResultPage
searchTerm={use(searchParams).searchTerm || ""}
sort={use(searchParams).sort || "trending"}
/>
);
}
function SearchResults({
searchTerm,
sort,
}: {
searchTerm: string;
sort: string;
}): React.ReactElement {
const [showAgents, setShowAgents] = useState(true);
const [showCreators, setShowCreators] = useState(true);
const [agents, setAgents] = useState<StoreAgent[]>([]);
const [creators, setCreators] = useState<Creator[]>([]);
const [isLoading, setIsLoading] = useState(true);
const api = useBackendAPI();
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const [agentsRes, creatorsRes] = await Promise.all([
api.getStoreAgents({
search_query: searchTerm,
sorted_by: sort,
}),
api.getStoreCreators({
search_query: searchTerm,
}),
]);
setAgents(agentsRes.agents || []);
setCreators(creatorsRes.creators || []);
} catch (error) {
console.error("Error fetching data:", error);
} finally {
setIsLoading(false);
}
};
fetchData();
}, [api, searchTerm, sort]);
const agentsCount = agents.length;
const creatorsCount = creators.length;
const totalCount = agentsCount + creatorsCount;
const handleFilterChange = (value: string) => {
if (value === "agents") {
setShowAgents(true);
setShowCreators(false);
} else if (value === "creators") {
setShowAgents(false);
setShowCreators(true);
} else {
setShowAgents(true);
setShowCreators(true);
}
};
const handleSortChange = useCallback(
(sortValue: string) => {
let sortBy = "recent";
if (sortValue === "runs") {
sortBy = "runs";
} else if (sortValue === "rating") {
sortBy = "rating";
}
const sortedAgents = [...agents].sort((a, b) => {
if (sortBy === "runs") {
return b.runs - a.runs;
} else if (sortBy === "rating") {
return b.rating - a.rating;
} else {
return (
new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
);
}
});
const sortedCreators = [...creators].sort((a, b) => {
if (sortBy === "runs") {
return b.agent_runs - a.agent_runs;
} else if (sortBy === "rating") {
return b.agent_rating - a.agent_rating;
} else {
// Creators don't have updated_at, sort by number of agents as fallback
return b.num_agents - a.num_agents;
}
});
setAgents(sortedAgents);
setCreators(sortedCreators);
},
[agents, creators],
);
return (
<div className="w-full">
<div className="mx-auto min-h-screen max-w-[1440px] px-10 lg:min-w-[1440px]">
<div className="mt-8 flex items-center">
<div className="flex-1">
<h2 className="text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
Results for:
</h2>
<h1 className="font-poppins text-2xl font-semibold leading-[32px] text-neutral-800 dark:text-neutral-100">
{searchTerm}
</h1>
</div>
<div className="flex-none">
<SearchBar width="w-[439px]" height="h-[60px]" />
</div>
</div>
{isLoading ? (
<div className="mt-20 flex flex-col items-center justify-center">
<p className="text-neutral-500 dark:text-neutral-400">Loading...</p>
</div>
) : totalCount > 0 ? (
<>
<div className="mt-[36px] flex items-center justify-between">
<SearchFilterChips
totalCount={totalCount}
agentsCount={agentsCount}
creatorsCount={creatorsCount}
onFilterChange={handleFilterChange}
/>
<SortDropdown onSort={handleSortChange} />
</div>
{/* Content section */}
<div className="min-h-[500px] max-w-[1440px]">
{showAgents && agentsCount > 0 && (
<div className="mt-[36px]">
<AgentsSection agents={agents} sectionTitle="Agents" />
</div>
)}
{showAgents && agentsCount > 0 && creatorsCount > 0 && (
<Separator />
)}
{showCreators && creatorsCount > 0 && (
<FeaturedCreators
featuredCreators={creators}
title="Creators"
/>
)}
</div>
</>
) : (
<div className="mt-20 flex flex-col items-center justify-center">
<h3 className="mb-2 text-xl font-medium text-neutral-600 dark:text-neutral-300">
No results found
</h3>
<p className="text-neutral-500 dark:text-neutral-400">
Try adjusting your search terms or filters
</p>
</div>
)}
</div>
</div>
);
}

View File

@@ -10,9 +10,9 @@ import {
import { ChevronDownIcon } from "@radix-ui/react-icons";
const sortOptions: SortOption[] = [
{ label: "Most Recent", value: "recent" },
// { label: "Most Recent", value: "recent" }, // we are not using this for now because we don't have date data from the backend
{ label: "Most Runs", value: "runs" },
{ label: "Highest Rated", value: "rating" },
// { label: "Highest Rated", value: "rating" }, // we are not using this for now because we don't have rating data from the backend
];
interface SortOption {

View File

@@ -98,7 +98,7 @@ export class MarketplacePage extends BasePage {
}
async searchAndNavigate(query: string, page: Page) {
const searchInput = await this.getSearchInput(page);
const searchInput = (await this.getSearchInput(page)).first();
await searchInput.fill(query);
await searchInput.press("Enter");
}