Vitron-Front/app/routes/_index.tsx
Arda Samadi 11656c2996 feat(home): seller brand chip (logo + name) on product tiles
Show the seller's brand on the home product cards, like the design:
a small logo circle + name overlaid above the title.

- new SellerChip component: renders nothing without a name; falls back to
  the seller's initial when no logo is present (e.g. before the backend
  ships seller_logo). seller_name is already returned today, so the name
  shows immediately.
- thread sellerName/sellerLogo/sellerUsername through ProductHomepage type,
  the home discounts + for-you mappings, and the MondrianProductList /
  HorizontalProductList card interfaces + overlays.

Only renders where seller data is mapped (home), so seller-page and
store-dashboard tiles are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 16:38:44 +03:30

334 lines
13 KiB
TypeScript
Raw 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 { MetaFunction } from "@remix-run/node";
import { Link, useSearchParams } from "@remix-run/react";
import { useEffect, useState } from "react";
import { ChevronLeft } from "lucide-react";
import HomePageTopBar from "~/components/home/HomePageTopBar";
import { ProductImageCarousel } from "~/components/product";
import {
useGetHomeBanners,
useGetHomeDiscounts,
useGetHomeForYou,
useGetHomeSellerTiles,
} from "~/requestHandler/use-home-hooks";
import { useCollectionsInfiniteQuery } from "~/requestHandler/use-collection-hooks";
import { Skeleton } from "~/components/ui/skeleton";
import MondrianProductList from "~/components/MondrianProductList";
import HorizontalProductList from "~/components/HorizontalProductList";
import HorizontalCollectionsList from "~/components/HorizontalCollectionsList";
import SellerTilesGrid from "~/components/SellerTilesGrid";
import InvoiceModal from "~/components/home/InvoiceModal";
export default function HomePage() {
const [searchParams] = useSearchParams();
const [showInvoiceModal, setShowInvoiceModal] = useState(false);
// Fetch home page data
const { data: banners, isLoading: isLoadingBanners } = useGetHomeBanners();
const {
data: discountsData,
isLoading: isLoadingDiscounts,
isError: isErrorDiscounts,
refetch: refetchDiscounts,
} = useGetHomeDiscounts();
const {
data: forYouData,
isLoading: isLoadingForYou,
isError: isErrorForYou,
refetch: refetchForYou,
fetchNextPage: fetchNextForYou,
hasNextPage: hasNextForYou,
isFetchingNextPage: isFetchingNextForYou,
} = useGetHomeForYou();
const {
data: sellerTilesData,
isLoading: isLoadingSellerTiles,
isError: isErrorSellerTiles,
refetch: refetchSellerTiles,
} = useGetHomeSellerTiles();
const {
data: collectionsData,
isLoading: isLoadingCollections,
isError: isErrorCollections,
refetch: refetchCollections,
} = useCollectionsInfiniteQuery({
pageSize: 10,
});
// Flatten the paginated data - extract results from each page
// For discounts, only get first page
const discounts = discountsData?.pages[0]?.results || [];
const forYouProducts =
forYouData?.pages.flatMap((page) => page?.results || []) || [];
const collections = collectionsData?.pages[0]?.results || [];
useEffect(() => {
// Check if the URL has an invoice parameter
if (searchParams.has("invoice") && searchParams.has("shippinginfo")) {
setShowInvoiceModal(true);
}
}, [searchParams]);
return (
<>
<div className="flex flex-col pb-12">
<div className="lg:hidden">
<HomePageTopBar />
</div>
{/* Adding padding to account for fixed header */}
<div className="flex flex-col gap-10 lg:gap-14 lg:max-w-[1320px] lg:w-full lg:mx-auto lg:px-8">
{/* Hero: mobile = full-bleed banner carousel; desktop = main + 2 promos */}
<div className="w-full mt-2 lg:mt-7 lg:grid lg:grid-cols-3 lg:gap-4">
<div className="h-[260px] lg:h-[408px] lg:col-span-2 lg:rounded-[22px] lg:overflow-hidden lg:shadow-md">
{isLoadingBanners ? (
<Skeleton className="w-full h-full" />
) : (
<ProductImageCarousel
images={banners?.map((banner) => banner.imageUrl || "") || []}
hideIndex={true}
borderRadius={0}
/>
)}
</div>
{/* Desktop-only side promos */}
<div className="hidden lg:grid lg:grid-rows-2 lg:gap-4">
<Link
to="/explore"
className="relative block rounded-[22px] overflow-hidden shadow-sm group"
>
<div className="absolute inset-0 bg-gradient-to-br from-[#6f7359] to-[#454935] transition-transform duration-500 group-hover:scale-105" />
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
<div className="absolute inset-x-0 bottom-0 p-5 text-white">
<small className="text-[12px] font-bold opacity-90">
تازه رسید
</small>
<h3 className="text-[20px] font-extrabold mt-1 leading-snug">
کالکشن جدید
</h3>
</div>
</Link>
<Link
to="/"
className="relative block rounded-[22px] overflow-hidden shadow-sm group"
>
<div className="absolute inset-0 bg-gradient-to-br from-[#b06a3c] to-[#7c4220] transition-transform duration-500 group-hover:scale-105" />
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
<div className="absolute inset-x-0 bottom-0 p-5 text-white">
<small className="text-[12px] font-bold opacity-90">
فروش ویژه
</small>
<h3 className="text-[20px] font-extrabold mt-1 leading-snug">
تا ۵۰٪ تخفیف
</h3>
</div>
</Link>
</div>
</div>
<section
id="section1"
className="w-full gap-2 flex flex-col max-w-[100vw] lg:max-w-none lg:gap-5"
>
<div className="w-full px-4 lg:px-0">
<p className="text-B16 lg:text-B24 font-bold">تخفیفها</p>
<p className="text-R14 lg:text-R16 text-GRAY">
فرصتهایی محدود برای انتخابهای هوشمندانه
</p>
</div>
<HorizontalProductList
products={
discounts?.map((product) => ({
id: product?.id || "",
title: product?.title || "",
images: Array.isArray(product?.imageUrls)
? product.imageUrls
: [],
discountPercent: product?.discountPercent || 0,
discountPrice: product?.discountPrice || 0,
basePrice: product?.basePrice || 0,
sellerName: product?.sellerName,
sellerLogo: product?.sellerLogo,
sellerUsername: product?.sellerUsername,
})) || []
}
isLoading={isLoadingDiscounts}
isError={isErrorDiscounts}
refetch={refetchDiscounts}
/>
</section>
<section
id="section2"
className="w-full gap-2 flex flex-col max-w-[100vw] lg:max-w-none lg:gap-5"
>
<div className="w-full px-4 lg:px-0 flex items-center justify-between">
<div>
<p className="text-B16 lg:text-B24 font-bold">برترین فروشگاهها</p>
<p className="text-R14 lg:text-R16 text-GRAY">
برگرفته از بالاترین سطح رضایت کاربران
</p>
</div>
<Link
to="/sellers"
className="flex items-center gap-1 text-R14 text-gray-500 hover:text-gray-800 hover:underline transition-colors group"
>
<span>مشاهده همه</span>
<ChevronLeft
size={16}
className="group-hover:-translate-x-1 transition-transform"
/>
</Link>
</div>
<SellerTilesGrid
sellers={sellerTilesData || []}
isLoading={isLoadingSellerTiles}
isError={isErrorSellerTiles}
refetch={refetchSellerTiles}
showViewAllButton={false}
/>
</section>
<section
id="collections"
className="w-full gap-2 flex flex-col max-w-[100vw] lg:max-w-none lg:gap-5"
>
<div className="w-full px-4 lg:px-0">
<p className="text-B16 lg:text-B24 font-bold">کالکشنها</p>
<p className="text-R14 lg:text-R16 text-GRAY">
مجموعههای منتخب از بهترین محصولات
</p>
</div>
<HorizontalCollectionsList
collections={
collections?.map((collection) => ({
id: collection?.id || "",
title: collection?.title || "",
description: collection?.description || "",
coverImageUrl: collection?.coverImageUrl || "",
productCount: collection?.productCount || 0,
})) || []
}
isLoading={isLoadingCollections}
isError={isErrorCollections}
refetch={refetchCollections}
showViewAllButton={true}
/>
</section>
<section
id="section3"
className="w-full gap-2 flex flex-col max-w-[100vw] lg:max-w-none lg:gap-5"
>
<div className="w-full px-4 lg:px-0">
<p className="text-B16 lg:text-B24 font-bold">برای شما</p>
<p className="text-R14 lg:text-R16 text-GRAY">
با الهام از انتخابهای شما
</p>
</div>
<MondrianProductList
products={
forYouProducts?.map((product) => ({
id: product?.id || "",
title: product?.title || "",
images: Array.isArray(product?.imageUrls)
? product.imageUrls
: [],
discountPercent: product?.discountPercent || 0,
discountPrice: product?.discountPrice || 0,
basePrice: product?.basePrice || 0,
sellerName: product?.sellerName,
sellerLogo: product?.sellerLogo,
sellerUsername: product?.sellerUsername,
})) || []
}
fetchNextPage={fetchNextForYou}
hasNextPage={hasNextForYou || false}
isFetchingNextPage={isFetchingNextForYou}
isLoading={isLoadingForYou}
isError={isErrorForYou}
refetch={refetchForYou}
emptyState={{}}
/>
</section>
</div>
</div>
<InvoiceModal
open={showInvoiceModal}
onOpenChange={setShowInvoiceModal}
/>
</>
);
}
export const meta: MetaFunction = () => {
const title = "ویترون | پلتفرم خرید آنلاین - محصولات باکیفیت با بهترین قیمت";
const description =
"ویترون بهترین پلتفرم خرید آنلاین در ایران. محصولات متنوع، قیمت مناسب، تحویل سریع. خرید امن از فروشگاه‌های معتبر با بالاترین کیفیت خدمات.";
const imageUrl =
"https://vitrownstatics.s3.ir-thr-at1.arvanstorage.ir/SVG-06.svg?versionId=";
const siteUrl = import.meta.env.VITE_SITE_URL || "https://vitrown.com/";
return [
{ title },
{ name: "description", content: description },
{
name: "keywords",
content:
"خرید آنلاین، فروشگاه اینترنتی، ویترون، محصولات باکیفیت، خرید امن، تحویل سریع",
},
{ name: "author", content: "Vitrown" },
{ name: "robots", content: "index, follow" },
// viewport is defined globally in root.tsx (with viewport-fit=cover for
// iOS safe-area handling); don't override it here or the home page loses
// edge-to-edge / notch support.
// Open Graph
{ property: "og:type", content: "website" },
{ property: "og:title", content: title },
{ property: "og:description", content: description },
{ property: "og:image", content: imageUrl },
{ property: "og:url", content: siteUrl },
{ 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 },
// Additional SEO
{ name: "theme-color", content: "#ffffff" },
{ name: "mobile-web-app-capable", content: "yes" },
{ name: "apple-mobile-web-app-status-bar-style", content: "default" },
{ name: "format-detection", content: "telephone=no" },
// Structured Data
{
"script:ld+json": {
"@context": "https://schema.org",
"@type": "WebSite",
name: "ویترون",
description: description,
url: siteUrl,
potentialAction: {
"@type": "SearchAction",
target: {
"@type": "EntryPoint",
urlTemplate: siteUrl + "/search/{search_term_string}",
},
"query-input": "required name=search_term_string",
},
publisher: {
"@type": "Organization",
name: "ویترون",
url: siteUrl,
},
},
},
];
};