← 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;