import * as React from 'react' import {createContext, useCallback, useContext, useMemo} from 'react' import {type Shop} from '@shopify/shop-minis-platform' import {Star} from 'lucide-react' import {useShopNavigation} from '../../hooks/navigation/useShopNavigation' import {cn} from '../../lib/utils' import { type ExtractedBrandTheme, extractBrandTheme, formatReviewCount, getFeaturedImages, normalizeRating, } from '../../utils' import {isDarkColor} from '../../utils/colors' import {Image} from '../atoms/image' import {Touchable} from '../atoms/touchable' interface MerchantCardContextValue { // Core data shop: Shop // Derived data cardTheme: ExtractedBrandTheme // UI configuration touchable: boolean featuredImagesLimit: number // Actions onClick: () => void } const MerchantCardContext = createContext( undefined ) function useMerchantCardContext() { const context = useContext(MerchantCardContext) if (!context) { throw new Error( 'useMerchantCardContext must be used within a MerchantCardProvider' ) } return context } function MerchantCardContainer({ className, ...props }: React.ComponentProps<'div'>) { const {touchable, cardTheme, onClick} = useMerchantCardContext() const content = (
) if (touchable && onClick) { return ( {content} ) } return content } function MerchantCardImage({ className, src, alt, thumbhash, ...props }: React.ComponentProps<'img'> & { src?: string alt?: string thumbhash?: string }) { if (!src) { return
} if (thumbhash) { return ( {alt} ) } return ( {alt} ) } function MerchantCardLogo({className, ...props}: React.ComponentProps<'div'>) { const {shop} = useMerchantCardContext() const {name, visualTheme} = shop const logoAverageColor = visualTheme?.brandSettings?.colors?.logoAverage const logoDominantColor = visualTheme?.brandSettings?.colors?.logoDominant const logoColor = logoAverageColor || logoDominantColor const logoBackgroundClassName = useMemo( () => (logoColor && isDarkColor(logoColor) ? 'bg-white' : 'bg-gray-800'), [logoColor] ) const logoUrl = visualTheme?.logoImage?.url const altText = `${name} logo` return (
{logoUrl ? ( {altText} ) : (
{name?.slice(0, 1)}
)}
) } function MerchantCardInfo({className, ...props}: React.ComponentProps<'div'>) { const {cardTheme} = useMerchantCardContext() const isDarkTheme = useMemo(() => { return ( cardTheme.backgroundColor !== 'white' && isDarkColor(cardTheme.backgroundColor) ) }, [cardTheme.backgroundColor]) const textColor = isDarkTheme ? 'text-primary-foreground' : 'text-foreground' return (
) } function MerchantCardName({ className, children, ...props }: React.ComponentProps<'h3'>) { const {shop} = useMerchantCardContext() const {name} = shop const nameContent = children ?? name return (

{nameContent}

) } function MerchantCardRating({ className, ...props }: React.ComponentProps<'div'> & { rating?: number | null reviewCount?: number }) { const {shop} = useMerchantCardContext() const { reviewAnalytics: {averageRating, reviewCount}, } = shop if (!averageRating || !reviewCount) return null return (
{normalizeRating(averageRating)} ({formatReviewCount(reviewCount)})
) } function MerchantCardDefaultHeader({withLogo = false}: {withLogo?: boolean}) { const {shop, cardTheme, featuredImagesLimit} = useMerchantCardContext() const {visualTheme} = shop const featuredImages = useMemo( () => getFeaturedImages(visualTheme, featuredImagesLimit), [visualTheme, featuredImagesLimit] ) const numberOfFeaturedImages = featuredImages?.length ?? 0 const displayDefaultCover = () => { if (numberOfFeaturedImages > 0) { const heightClass = numberOfFeaturedImages === 2 ? 'h-full' : 'h-1/2' return featuredImages?.map((image, index) => (
)) } else if (cardTheme.type === 'coverImage') { return ( ) } return null } return (
{withLogo && (
)} {displayDefaultCover()}
) } function MerchantCardBrandedHeader({withLogo = false}: {withLogo?: boolean}) { const {shop, cardTheme} = useMerchantCardContext() const wordmarkImage = shop.visualTheme?.brandSettings?.headerTheme?.wordmark return (
{cardTheme.type === 'coverImage' && ( <>
)} {withLogo && (
{wordmarkImage ? ( {wordmarkImage.altText ) : ( )}
)}
) } interface MerchantCardHeaderProps { isDefault?: boolean withLogo?: boolean } function MerchantCardHeader({ isDefault, withLogo, className, ...props }: React.ComponentProps<'div'> & MerchantCardHeaderProps) { const {cardTheme} = useMerchantCardContext() const isBranded = cardTheme.type === 'coverImage' || cardTheme.type === 'brandColor' return (
{isBranded && !isDefault ? ( ) : ( )}
) } /** * Use when showcasing merchants as destinations, not just product sources. Automatically adapts to merchant's brand colors, logo placement, and featured product images. Respects merchant brand guidelines while maintaining consistent Shop UI patterns. * @publicDocs */ export interface MerchantCardProps { /** The shop/merchant to display */ shop: Shop /** Whether the card is tappable to navigate to shop (default: true) */ touchable?: boolean /** Maximum number of featured product images to show (default: 4) */ featuredImagesLimit?: number /** Custom content to render inside the card */ children?: React.ReactNode } function MerchantCard({ shop, touchable = true, featuredImagesLimit = 4, children, }: MerchantCardProps) { const {navigateToShop} = useShopNavigation() const {id, visualTheme} = shop const handleClick = useCallback(() => { if (!touchable) return navigateToShop({shopId: id}) }, [navigateToShop, id, touchable]) const cardTheme = useMemo( () => extractBrandTheme(visualTheme?.brandSettings), [visualTheme?.brandSettings] ) const contextValue = useMemo( () => ({ shop, cardTheme, touchable, featuredImagesLimit, onClick: handleClick, }), [shop, cardTheme, touchable, featuredImagesLimit, handleClick] ) return ( {children ?? ( )} ) } export { MerchantCard, MerchantCardContainer, MerchantCardHeader, MerchantCardInfo, MerchantCardName, MerchantCardRating, }