Vitron-Front/app/routes/search.$query.tsx
Arda Samadi 8b51c3252a 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>
2026-07-09 10:44:52 +03:30

178 lines
6.3 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from "react";
import { MetaFunction } from "@remix-run/node";
import { Link, useNavigate, useParams } from "@remix-run/react";
import { onSearch } from "../utils/helpers";
import { PackageSearch } from "lucide-react";
import { useSearchInfinite } from "../requestHandler/use-search-hooks";
import MondrianProductList from "~/components/MondrianProductList";
import SearchTopBar from "~/components/SearchTopBar";
import SellerLogo from "~/components/SellerLogo";
import HorizontalCollectionsList from "~/components/HorizontalCollectionsList";
export default function Search() {
const { query } = useParams();
const navigate = useNavigate();
const [searchValue, setSearchValue] = useState(query || "");
// Update search value when route parameter changes
useEffect(() => {
if (query) {
setSearchValue(query);
}
}, [query]);
// Use infinite search hook to fetch data, passing the route query parameter
const {
data,
isLoading,
isError,
refetch,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = 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 (
<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">
<SearchTopBar
searchValue={searchValue}
setSearchValue={setSearchValue}
placeholder="جستجو..."
minimumPrice={320000}
maximumPrice={1000000}
onSearch={() => onSearch(searchValue, navigate)}
/>
</div>
{/* Desktop results heading */}
<div className="hidden lg:block">
<nav className="flex items-center gap-2 text-[13px] text-GRAY">
<Link to="/" className="hover:text-BLACK">
خانه
</Link>
<span>/</span>
<span>جستجو</span>
</nav>
<h1 className="text-[26px] font-extrabold mt-2">
نتایج برای «<span className="text-VITROWN_BLUE">{query}</span>»
</h1>
</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
products={
data?.pages
.flatMap((page) => page.products)
.map((product) => {
return {
id: product.id,
title: product.title,
images: product.imageUrls || [],
discountPercent: product.discountPercent || 0,
discountPrice:
product.discountPrice || product.price || 0,
basePrice: product.basePrice || product.price || 0,
sellerName: product.sellerName,
};
}) || []
}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
isLoading={isLoading}
isError={isError}
refetch={refetch}
emptyState={{
image: <PackageSearch size={80} />,
title: "محصولی در این جستجو یافت نشد",
description: "جستجوی دیگری انجام دهید یا فیلترها را تغییر دهید",
}}
/>
</div>
);
}
export const meta: MetaFunction = ({ params }) => {
const query = params.query;
const title = query ? `جستجو: ${query} | ویترون` : "جستجو | ویترون";
const description = query
? `نتایج جستجو برای "${query}" در ویترون - یافتن بهترین محصولات با قیمت مناسب`
: "جستجو در ویترون - یافتن محصولات مورد نظر با امکانات پیشرفته فیلتر و مرتب‌سازی";
const imageUrl =
"https://vitrownstatics.s3.ir-thr-at1.arvanstorage.ir/SVG-06.svg?versionId=";
return [
{ title },
{ name: "description", content: description },
{
name: "keywords",
content: `جستجو، ${query || ""}, محصولات، ویترون، فیلتر`,
},
{ name: "robots", content: "index, follow" },
// Open Graph
{ property: "og:type", content: "website" },
{ property: "og:title", content: title },
{ property: "og:description", content: description },
{ property: "og:image", content: imageUrl },
{ property: "og:site_name", content: "ویترون" },
{ property: "og:locale", content: "fa_IR" },
// Twitter Card
{ name: "twitter:card", content: "summary_large_image" },
{ name: "twitter:title", content: title },
{ name: "twitter:description", content: description },
{ name: "twitter:image", content: imageUrl },
];
};