mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-10 07:38:04 -05:00
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:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user