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>
334 lines
13 KiB
TypeScript
334 lines
13 KiB
TypeScript
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,
|
||
},
|
||
},
|
||
},
|
||
];
|
||
};
|