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>
This commit is contained in:
parent
692d385d09
commit
11656c2996
@ -6,6 +6,7 @@ import { Skeleton } from "./ui/skeleton";
|
|||||||
import { ErrorState } from "./UiProvider";
|
import { ErrorState } from "./UiProvider";
|
||||||
import discountRed from "~/assets/icons/product/discount-red.svg";
|
import discountRed from "~/assets/icons/product/discount-red.svg";
|
||||||
import placeholderImage from "~/assets/icons/product.png";
|
import placeholderImage from "~/assets/icons/product.png";
|
||||||
|
import SellerChip from "./SellerChip";
|
||||||
|
|
||||||
interface HorizontalProduct {
|
interface HorizontalProduct {
|
||||||
id?: string;
|
id?: string;
|
||||||
@ -14,6 +15,9 @@ interface HorizontalProduct {
|
|||||||
discountPercent?: number;
|
discountPercent?: number;
|
||||||
discountPrice?: number;
|
discountPrice?: number;
|
||||||
basePrice?: number;
|
basePrice?: number;
|
||||||
|
sellerName?: string;
|
||||||
|
sellerLogo?: string | null;
|
||||||
|
sellerUsername?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HorizontalProductList = ({
|
const HorizontalProductList = ({
|
||||||
@ -138,6 +142,10 @@ const HorizontalProductList = ({
|
|||||||
)}
|
)}
|
||||||
{/* Overlay info */}
|
{/* Overlay info */}
|
||||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 via-black/40 to-transparent p-2 pt-6 pointer-events-none">
|
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 via-black/40 to-transparent p-2 pt-6 pointer-events-none">
|
||||||
|
<SellerChip
|
||||||
|
name={product.sellerName}
|
||||||
|
logo={product.sellerLogo}
|
||||||
|
/>
|
||||||
<div className="flex w-full justify-between gap-1">
|
<div className="flex w-full justify-between gap-1">
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<h3 className="text-white text-xs font-semibold line-clamp-1 mb-0.5">
|
<h3 className="text-white text-xs font-semibold line-clamp-1 mb-0.5">
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import placeholderImage from "~/assets/icons/product.png";
|
|||||||
import placeholderImageDark from "~/assets/icons/product-dark.png";
|
import placeholderImageDark from "~/assets/icons/product-dark.png";
|
||||||
import { useRootData } from "~/hooks/use-root-data";
|
import { useRootData } from "~/hooks/use-root-data";
|
||||||
import discountRed from "~/assets/icons/product/discount-red.svg";
|
import discountRed from "~/assets/icons/product/discount-red.svg";
|
||||||
|
import SellerChip from "./SellerChip";
|
||||||
|
|
||||||
interface MondrianList {
|
interface MondrianList {
|
||||||
id?: string;
|
id?: string;
|
||||||
@ -21,6 +22,9 @@ interface MondrianList {
|
|||||||
discountPercent?: number;
|
discountPercent?: number;
|
||||||
discountPrice?: number;
|
discountPrice?: number;
|
||||||
basePrice?: number;
|
basePrice?: number;
|
||||||
|
sellerName?: string;
|
||||||
|
sellerLogo?: string | null;
|
||||||
|
sellerUsername?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility function to detect media type
|
// Utility function to detect media type
|
||||||
@ -400,6 +404,7 @@ const ProductCard = ({
|
|||||||
)}
|
)}
|
||||||
{/* Overlay info - Airbnb style */}
|
{/* Overlay info - Airbnb style */}
|
||||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 via-black/40 to-transparent p-2 pt-6 pointer-events-none">
|
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 via-black/40 to-transparent p-2 pt-6 pointer-events-none">
|
||||||
|
<SellerChip name={product.sellerName} logo={product.sellerLogo} />
|
||||||
<div className="flex w-full justify-between gap-1">
|
<div className="flex w-full justify-between gap-1">
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<h3 className="text-white text-xs font-semibold line-clamp-1 mb-0.5">
|
<h3 className="text-white text-xs font-semibold line-clamp-1 mb-0.5">
|
||||||
|
|||||||
32
app/components/SellerChip.tsx
Normal file
32
app/components/SellerChip.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
interface SellerChipProps {
|
||||||
|
name?: string;
|
||||||
|
logo?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Small brand chip (logo + name) shown over a product card's image overlay.
|
||||||
|
* Renders nothing when there's no seller name. Falls back to the seller's
|
||||||
|
* initial when no logo is available (e.g. before the backend exposes it).
|
||||||
|
*/
|
||||||
|
export default function SellerChip({ name, logo }: SellerChipProps) {
|
||||||
|
if (!name) return null;
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1.5 mb-1 min-w-0">
|
||||||
|
<span className="w-[15px] h-[15px] rounded-full overflow-hidden border border-white/40 bg-white/20 shrink-0 grid place-items-center text-[8px] font-bold text-white">
|
||||||
|
{logo ? (
|
||||||
|
<img
|
||||||
|
src={logo}
|
||||||
|
alt=""
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
onError={(e) => {
|
||||||
|
e.currentTarget.style.display = "none";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
name.trim().charAt(0)
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="text-white/85 text-[10.5px] truncate">{name}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -151,6 +151,9 @@ export default function HomePage() {
|
|||||||
discountPercent: product?.discountPercent || 0,
|
discountPercent: product?.discountPercent || 0,
|
||||||
discountPrice: product?.discountPrice || 0,
|
discountPrice: product?.discountPrice || 0,
|
||||||
basePrice: product?.basePrice || 0,
|
basePrice: product?.basePrice || 0,
|
||||||
|
sellerName: product?.sellerName,
|
||||||
|
sellerLogo: product?.sellerLogo,
|
||||||
|
sellerUsername: product?.sellerUsername,
|
||||||
})) || []
|
})) || []
|
||||||
}
|
}
|
||||||
isLoading={isLoadingDiscounts}
|
isLoading={isLoadingDiscounts}
|
||||||
@ -235,6 +238,9 @@ export default function HomePage() {
|
|||||||
discountPercent: product?.discountPercent || 0,
|
discountPercent: product?.discountPercent || 0,
|
||||||
discountPrice: product?.discountPrice || 0,
|
discountPrice: product?.discountPrice || 0,
|
||||||
basePrice: product?.basePrice || 0,
|
basePrice: product?.basePrice || 0,
|
||||||
|
sellerName: product?.sellerName,
|
||||||
|
sellerLogo: product?.sellerLogo,
|
||||||
|
sellerUsername: product?.sellerUsername,
|
||||||
})) || []
|
})) || []
|
||||||
}
|
}
|
||||||
fetchNextPage={fetchNextForYou}
|
fetchNextPage={fetchNextForYou}
|
||||||
|
|||||||
@ -3813,6 +3813,18 @@ export interface ProductHomepage {
|
|||||||
* @memberof ProductHomepage
|
* @memberof ProductHomepage
|
||||||
*/
|
*/
|
||||||
readonly sellerName?: string;
|
readonly sellerName?: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof ProductHomepage
|
||||||
|
*/
|
||||||
|
readonly sellerUsername?: string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof ProductHomepage
|
||||||
|
*/
|
||||||
|
readonly sellerLogo?: string | null;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user