← back to Handbag Auth Nextjs

src/components/HandbagCompareOffers.tsx

182 lines

// React component for comparing handbag prices across multiple merchants
// Displays offers sorted by price, with condition badges and affiliate links

import React from "react";

type Condition = "new" | "used" | "auction";

export interface HandbagOffer {
  id: string;
  merchantName: string;
  condition: Condition;
  price?: number;
  currency?: string;
  url: string; // affiliate-decorated link
  label?: string; // "Best price", "Auction", "Certified authentic", etc.
  logoUrl?: string;
}

interface Props {
  modelName: string;
  brand?: string;
  offers: HandbagOffer[];
  onOfferClick?: (offer: HandbagOffer) => void;
}

const conditionOrder: Condition[] = ["new", "used", "auction"];

const conditionColors = {
  new: "bg-emerald-100 text-emerald-800",
  used: "bg-blue-100 text-blue-800",
  auction: "bg-purple-100 text-purple-800",
};

export const HandbagCompareOffers: React.FC<Props> = ({
  modelName,
  brand,
  offers,
  onOfferClick,
}) => {
  if (!offers?.length) {
    return (
      <div className="rounded-xl border border-dashed border-gray-300 bg-gray-50 p-6 text-center">
        <p className="text-sm text-gray-600">
          No offers available for this handbag yet.
        </p>
        <p className="mt-2 text-xs text-gray-500">
          Check back soon or search other models
        </p>
      </div>
    );
  }

  // Sort by price (lowest first), then by condition
  const sorted = [...offers].sort((a, b) => {
    if (a.price != null && b.price != null) return a.price - b.price;
    if (a.price != null) return -1;
    if (b.price != null) return 1;
    return conditionOrder.indexOf(a.condition) - conditionOrder.indexOf(b.condition);
  });

  const bestOffer = sorted[0];

  return (
    <section className="mt-6 rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
      <header className="mb-4 flex items-start justify-between">
        <div>
          <h2 className="text-lg font-semibold text-gray-900">Compare Prices</h2>
          <p className="mt-1 text-sm text-gray-600">
            {brand && <span className="font-medium">{brand}</span>}
            {brand && " "}
            {modelName}
          </p>
          <p className="mt-1 text-xs text-gray-500">{offers.length} offers found</p>
        </div>
        {bestOffer?.price && (
          <div className="rounded-xl bg-gradient-to-br from-emerald-50 to-emerald-100 px-4 py-2 text-right shadow-sm">
            <p className="text-[10px] font-semibold uppercase tracking-wide text-emerald-700">
              Lowest Price
            </p>
            <p className="mt-1 text-xl font-bold text-emerald-900">
              {bestOffer.currency ?? "$"}
              {bestOffer.price.toLocaleString()}
            </p>
          </div>
        )}
      </header>

      <ul className="space-y-3">
        {sorted.map((offer) => {
          const isBestPrice = offer.id === bestOffer?.id && offer.price != null;

          return (
            <li
              key={offer.id}
              className={`flex items-center justify-between rounded-xl px-4 py-3.5 transition-all ${
                isBestPrice
                  ? "bg-gradient-to-r from-emerald-50 to-emerald-100 ring-2 ring-emerald-200"
                  : "bg-gray-50 hover:bg-gray-100"
              }`}
            >
              <div className="flex min-w-0 flex-1 items-center gap-3">
                {/* Merchant logo or initial */}
                {offer.logoUrl ? (
                  <img
                    src={offer.logoUrl}
                    alt={offer.merchantName}
                    className="h-9 w-9 rounded-full border-2 border-white object-cover shadow-sm"
                  />
                ) : (
                  <div className="flex h-9 w-9 items-center justify-center rounded-full bg-gradient-to-br from-gray-400 to-gray-500 text-xs font-bold text-white shadow-sm">
                    {offer.merchantName.slice(0, 2).toUpperCase()}
                  </div>
                )}

                <div className="min-w-0 flex-1">
                  <div className="flex flex-wrap items-center gap-2">
                    <p className="truncate font-medium text-gray-900">
                      {offer.merchantName}
                    </p>

                    {/* Condition badge */}
                    <span
                      className={`rounded-full px-2.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide ${
                        conditionColors[offer.condition]
                      }`}
                    >
                      {offer.condition}
                    </span>

                    {/* Special labels */}
                    {isBestPrice && (
                      <span className="rounded-full bg-gradient-to-r from-amber-400 to-orange-500 px-2.5 py-0.5 text-[10px] font-bold uppercase tracking-wide text-white shadow-sm">
                        Best Price
                      </span>
                    )}
                    {offer.label && !isBestPrice && (
                      <span className="rounded-full bg-amber-100 px-2.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-800">
                        {offer.label}
                      </span>
                    )}
                  </div>
                </div>
              </div>

              {/* Price and CTA */}
              <div className="ml-3 flex flex-col items-end gap-2">
                {offer.price ? (
                  <p className="text-lg font-bold text-gray-900">
                    {(offer.currency ?? "$")}
                    {offer.price.toLocaleString()}
                  </p>
                ) : (
                  <p className="text-xs font-medium text-gray-500">See price</p>
                )}

                <a
                  href={offer.url}
                  target="_blank"
                  rel="noopener noreferrer"
                  onClick={() => onOfferClick?.(offer)}
                  className="rounded-full bg-black px-4 py-1.5 text-xs font-semibold text-white transition-all hover:bg-gray-800 active:scale-95"
                >
                  {offer.condition === "auction" ? "Bid Now" : "View Offer"}
                </a>
              </div>
            </li>
          );
        })}
      </ul>

      <footer className="mt-4 border-t border-gray-200 pt-3">
        <p className="text-[11px] text-gray-500">
          Prices shown are as listed by merchants and may change. Some links are affiliate
          links, which help support our authentication service at no extra cost to you.
        </p>
      </footer>
    </section>
  );
};

export default HandbagCompareOffers;