feat(search): show sellers and collections in search results

Render a فروشگاه‌ها row (store cards with logo via SellerLogo, linking to the
store) and a کالکشن‌ها row (reusing HorizontalCollectionsList) above the product
grid on the search page. Sellers/collections read off the first page (they don't
paginate); products paginate as before. Types extended (SellerSearchResult.logo,
SearchResponse.collections). Degrades gracefully on older backends.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Arda Samadi 2026-07-09 10:44:52 +03:30
parent fea80647fa
commit 8b51c3252a
2 changed files with 76 additions and 0 deletions

View File

@ -6,6 +6,8 @@ import { PackageSearch } from "lucide-react";
import { useSearchInfinite } from "../requestHandler/use-search-hooks"; import { useSearchInfinite } from "../requestHandler/use-search-hooks";
import MondrianProductList from "~/components/MondrianProductList"; import MondrianProductList from "~/components/MondrianProductList";
import SearchTopBar from "~/components/SearchTopBar"; import SearchTopBar from "~/components/SearchTopBar";
import SellerLogo from "~/components/SellerLogo";
import HorizontalCollectionsList from "~/components/HorizontalCollectionsList";
export default function Search() { export default function Search() {
const { query } = useParams(); const { query } = useParams();
@ -30,6 +32,13 @@ export default function Search() {
isFetchingNextPage, isFetchingNextPage,
} = useSearchInfinite(query); } = useSearchInfinite(query);
// Sellers & collections are the same for every page of a query, so read them
// off the first page. Products paginate as usual.
const firstPage = data?.pages?.[0];
const sellers = firstPage?.sellers ?? [];
const collections = firstPage?.collections ?? [];
const hasNonProduct = sellers.length > 0 || collections.length > 0;
return ( return (
<div className="flex flex-col gap-6 bg-WHITE lg:max-w-[1320px] lg:mx-auto lg:w-full lg:px-8 lg:py-6"> <div className="flex flex-col gap-6 bg-WHITE lg:max-w-[1320px] lg:mx-auto lg:w-full lg:px-8 lg:py-6">
<div className="lg:hidden"> <div className="lg:hidden">
@ -57,6 +66,49 @@ export default function Search() {
</h1> </h1>
</div> </div>
{/* Sellers */}
{sellers.length > 0 && (
<section className="flex flex-col gap-3">
<p className="text-B16 lg:text-B24 font-bold px-4 lg:px-0">فروشگاهها</p>
<div className="flex gap-4 overflow-x-auto px-4 lg:px-0 pb-1">
{sellers.map((seller) => (
<Link
key={seller.id}
to={`/seller/${seller.id}`}
className="flex flex-col items-center gap-2 shrink-0 w-[76px]"
>
<SellerLogo src={seller.logo} alt={seller.name} size="xl" />
<p className="text-R12 font-bold text-center line-clamp-1 w-full">
{seller.name}
</p>
</Link>
))}
</div>
</section>
)}
{/* Collections */}
{collections.length > 0 && (
<section className="flex flex-col gap-3">
<p className="text-B16 lg:text-B24 font-bold px-4 lg:px-0">کالکشنها</p>
<HorizontalCollectionsList
collections={collections.map((c) => ({
id: c.id,
title: c.title,
coverImageUrl: c.coverImageUrl,
productCount: c.productCount,
}))}
isLoading={false}
isError={false}
refetch={refetch}
/>
</section>
)}
{hasNonProduct && (
<p className="text-B16 lg:text-B24 font-bold px-4 lg:px-0">محصولات</p>
)}
<MondrianProductList <MondrianProductList
products={ products={
data?.pages data?.pages

View File

@ -4329,6 +4329,24 @@ export interface SearchResponse {
* @memberof SearchResponse * @memberof SearchResponse
*/ */
sellers: Array<SellerSearchResult>; sellers: Array<SellerSearchResult>;
/**
* Public collections matching the query.
* @type {Array<CollectionSearchResult>}
* @memberof SearchResponse
*/
collections?: Array<CollectionSearchResult>;
}
/**
* A collection result in search.
* @export
* @interface CollectionSearchResult
*/
export interface CollectionSearchResult {
type?: string;
id: string;
title: string;
coverImageUrl?: string | null;
productCount?: number;
} }
/** /**
* *
@ -5546,6 +5564,12 @@ export interface SellerSearchResult {
* @memberof SellerSearchResult * @memberof SellerSearchResult
*/ */
id: string; id: string;
/**
* Store logo URL (may be null).
* @type {string}
* @memberof SellerSearchResult
*/
logo?: string | null;
} }
/** /**
* *