Compare Card

Installation

Add the following Soul components

The compare-card component uses the skeleton, product-card, rating and button components. Make sure you have added them to your project.

Install the following dependencies

npm install clsx

Copy and paste the following code into your project

primitives/compare-card/index.tsx

import { clsx } from 'clsx';import {  type Product,  ProductCard,  ProductCardSkeleton,} from '@/vibes/soul/primitives/product-card';import { Rating } from '@/vibes/soul/primitives/rating';import * as Skeleton from '@/vibes/soul/primitives/skeleton';import { ButtonLink } from '@/vibes/soul/primitives/button-link';import { AddToCartForm, CompareAddToCartAction } from './add-to-cart-form';export interface CompareProduct extends Product {  description?: string | React.ReactNode;  customFields?: Array<{ name: string; value: string }>;  hasVariants?: boolean;  disabled?: boolean;  isPreorder?: boolean;}export interface CompareCardProps {  className?: string;  product: CompareProduct;  addToCartLabel?: string;  descriptionLabel?: string;  noDescriptionLabel?: string;  ratingLabel?: string;  noRatingsLabel?: string;  otherDetailsLabel?: string;  noOtherDetailsLabel?: string;  viewOptionsLabel?: string;  preorderLabel?: string;  addToCartAction?: CompareAddToCartAction;  imageSizes?: string;}/** * This component supports various CSS variables for theming. Here's a comprehensive list, along * with their default values: * * ```css * :root { *   --compare-card-divider: hsl(var(--contrast-100)); *   --compare-card-label: hsl(var(--foreground)); *   --compare-card-description: hsl(var(--contrast-400)); *   --compare-card-field: hsl(var(--foreground)); *   --compare-card-font-family-primary: var(--font-family-body); *   --compare-card-font-family-secondary: var(--font-family-mono); * } * ``` */export function CompareCard({  className,  product,  addToCartAction,  addToCartLabel = 'Add to cart',  descriptionLabel = 'Description',  noDescriptionLabel = 'There is no description available.',  ratingLabel = 'Rating',  noRatingsLabel = 'There are no reviews.',  otherDetailsLabel = 'Other details',  noOtherDetailsLabel = 'There are no other details.',  viewOptionsLabel = 'View options',  preorderLabel = 'Preorder',  imageSizes,}: CompareCardProps) {  return (    <div      className={clsx(        'w-full max-w-md divide-y divide-[var(--compare-card-divider,hsl(var(--contrast-100)))] font-[family-name:var(--compare-card-font-family-primary,var(--font-family-body))] font-normal @container',        className,      )}    >      <div className="mb-2 space-y-4 pb-4">        <ProductCard imageSizes={imageSizes} product={product} />        {addToCartAction &&          (product.hasVariants !== undefined && !product.hasVariants ? (            <AddToCartForm              addToCartAction={addToCartAction}              addToCartLabel={addToCartLabel}              disabled={product.disabled}              isPreorder={product.isPreorder}              preorderLabel={preorderLabel}              productId={product.id}            />          ) : (            <ButtonLink className="w-full" href={product.href} size="medium">              {viewOptionsLabel}            </ButtonLink>          ))}      </div>      <div className="space-y-4 py-4">        <div className="font-[family-name:var(--compare-card-font-family-secondary,var(--font-family-mono))] text-xs font-normal uppercase text-[var(--compare-card-label,hsl(var(--foreground)))]">          {ratingLabel}        </div>        {product.rating != null ? (          <Rating rating={product.rating} />        ) : (          <p className="text-sm text-[var(--compare-card-description,hsl(var(--contrast-400)))]">            {noRatingsLabel}          </p>        )}      </div>      <div className="space-y-4 py-4">        <div className="font-[family-name:var(--compare-card-font-family-secondary,var(--font-family-mono))] text-xs font-normal uppercase text-[var(--compare-card-label,hsl(var(--foreground)))]">          {descriptionLabel}        </div>        {product.description != null && product.description !== '' ? (          <div className="text-sm text-[var(--compare-card-description,hsl(var(--contrast-400)))]">            {product.description}          </div>        ) : (          <p className="text-sm text-[var(--compare-card-description,hsl(var(--contrast-400)))]">            {noDescriptionLabel}          </p>        )}      </div>      {product.customFields != null ? (        <div className="space-y-4 py-4">          <div className="font-[family-name:var(--compare-card-font-family-secondary,var(--font-family-mono))] text-xs font-normal uppercase text-[var(--compare-card-label,hsl(var(--foreground)))]">            {otherDetailsLabel}          </div>          {product.customFields.map((field, index) => (            <div key={index}>              <p className="text-xs font-normal text-[var(--compare-card-field,hsl(var(--foreground)))]">                <strong>{field.name}</strong>: {field.value}              </p>            </div>          ))}        </div>      ) : (        <div className="space-y-4 py-4">          <div className="font-[family-name:var(--compare-card-font-family-secondary,var(--font-family-mono))] text-xs font-normal uppercase text-[var(--compare-card-label,hsl(var(--foreground)))]">            {otherDetailsLabel}          </div>          <p className="text-sm text-[var(--compare-card-description,hsl(var(--contrast-400)))]">            {noOtherDetailsLabel}          </p>        </div>      )}    </div>  );}export function CompareCardSkeleton({ className }: { className?: string }) {  return (    <div      className={clsx(        'w-full max-w-md divide-y divide-[var(--skeleton,hsl(var(--contrast-300)/15%))] @container',        className,      )}    >      <div className="mb-2 space-y-4 pb-4">        <ProductCardSkeleton />        <Skeleton.Box className="h-12 rounded-full" />      </div>      <div className="space-y-4 py-4 text-xs">        <Skeleton.Text characterCount={10} className="rounded" />        <Skeleton.Box className="h-6 w-32 rounded" />      </div>      <div className="space-y-4 py-4 text-xs">        <Skeleton.Text characterCount={12} className="rounded" />        <div className="text-sm">          <Skeleton.Text characterCount="full" className="rounded" />          <Skeleton.Text characterCount={45} className="rounded" />          <Skeleton.Text characterCount={40} className="rounded" />        </div>      </div>    </div>  );}

Usage

import { CompareCard } from '@/vibes/soul/primitives/compare-card';function Usage() {  return (    <CompareCard      imageSizes="(min-width: 42rem) 25vw, (min-width: 32rem) 33vw, (min-width: 28rem) 50vw, 100vw"      product={{        id: '1',        href: '#',        title: 'Mini Bar Bag',        image: {          src: 'https://rstr.in/monogram/vibes/mrlTNE1TJfB',          alt: 'Mini Bar Bag',        },        subtitle: 'Blue/Green',        price: '$65',        badge: 'New',      }}    />  );}

API Reference

CompareCardProps

PropTypeDefault
children*
ReactNode
className
string
product*
CompareProduct
addToCartLabel
string
'Add to cart'
descriptionLabel
string
'Description'
ratingLabel
string
'Rating'
otherDetailsLabel
string
'Other details'
addToCartAction
(id: string) => Promise<void>
imageSizes
string

This component uses the Next.js Image component. The imageSizes prop is passed directly to the sizes prop of the Next.js Image component. For more details, refer to the Next.js Image documentation.

CompareProduct

PropTypeDefault
description
string
customFields
Array<{ name: string; value: string }>

CSS Variables

This component supports various CSS variables for theming. Here's a comprehensive list.

:root {  --compare-card-divider: hsl(var(--contrast-100));  --compare-card-label: hsl(var(--foreground));  --compare-card-description: hsl(var(--contrast-400));  --compare-card-field: hsl(var(--foreground));  --compare-card-font-family-primary: var(--font-family-body);  --compare-card-font-family-secondary: var(--font-family-mono);}