163 lines
5.7 KiB
TypeScript
163 lines
5.7 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import type { PriceOffer, PriceSearchResponse } from "@/src/types/priceSearch";
|
||
|
|
|
||
|
|
const CONFIDENCE_LABELS: Record<PriceOffer["confidence"], string> = {
|
||
|
|
high: "Alta",
|
||
|
|
medium: "Media",
|
||
|
|
low: "Bassa",
|
||
|
|
};
|
||
|
|
|
||
|
|
const CONFIDENCE_STYLES: Record<PriceOffer["confidence"], string> = {
|
||
|
|
high: "bg-green-500/15 text-green-400 border-green-500/30",
|
||
|
|
medium: "bg-yellow-500/15 text-yellow-400 border-yellow-500/30",
|
||
|
|
low: "bg-zinc-500/15 text-zinc-400 border-zinc-500/30",
|
||
|
|
};
|
||
|
|
|
||
|
|
function formatPrice(price: number | null, currency: string) {
|
||
|
|
if (price === null) return "N/D";
|
||
|
|
return new Intl.NumberFormat("it-IT", {
|
||
|
|
style: "currency",
|
||
|
|
currency,
|
||
|
|
}).format(price);
|
||
|
|
}
|
||
|
|
|
||
|
|
interface PriceSearchPanelProps {
|
||
|
|
isLoading: boolean;
|
||
|
|
error: string | null;
|
||
|
|
result: PriceSearchResponse | null;
|
||
|
|
onApplyPrice: (price: number) => void;
|
||
|
|
onCancel: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function PriceSearchPanel({
|
||
|
|
isLoading,
|
||
|
|
error,
|
||
|
|
result,
|
||
|
|
onApplyPrice,
|
||
|
|
onCancel,
|
||
|
|
}: PriceSearchPanelProps) {
|
||
|
|
if (!isLoading && !error && !result) return null;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="p-4 rounded-lg bg-blue-500/5 border border-blue-500/30 space-y-3">
|
||
|
|
<div className="flex items-center justify-between gap-3">
|
||
|
|
<h3 className="text-sm font-semibold text-blue-400 flex items-center gap-2">
|
||
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth={2}
|
||
|
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
Ricerca prezzi online
|
||
|
|
</h3>
|
||
|
|
{isLoading && (
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={onCancel}
|
||
|
|
className="text-xs text-zinc-400 hover:text-white transition-colors"
|
||
|
|
>
|
||
|
|
Annulla
|
||
|
|
</button>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{isLoading && (
|
||
|
|
<div className="flex items-center gap-3 text-sm text-zinc-400">
|
||
|
|
<svg className="w-4 h-4 animate-spin text-blue-400" viewBox="0 0 24 24">
|
||
|
|
<circle
|
||
|
|
className="opacity-25"
|
||
|
|
cx="12"
|
||
|
|
cy="12"
|
||
|
|
r="10"
|
||
|
|
stroke="currentColor"
|
||
|
|
strokeWidth="4"
|
||
|
|
fill="none"
|
||
|
|
/>
|
||
|
|
<path
|
||
|
|
className="opacity-75"
|
||
|
|
fill="currentColor"
|
||
|
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
Ricerca in corso… può richiedere fino a 30 secondi
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{error && (
|
||
|
|
<div className="p-3 rounded-lg bg-red-500/10 border border-red-500/50 text-red-400 text-sm">
|
||
|
|
{error}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{result && (
|
||
|
|
<>
|
||
|
|
<p className="text-xs text-zinc-500">
|
||
|
|
Query: <span className="text-zinc-400">{result.query}</span>
|
||
|
|
</p>
|
||
|
|
|
||
|
|
{result.offers.length === 0 ? (
|
||
|
|
<p className="text-sm text-zinc-400">Nessuna offerta trovata.</p>
|
||
|
|
) : (
|
||
|
|
<ul className="space-y-2 max-h-48 overflow-y-auto">
|
||
|
|
{result.offers.map((offer, index) => (
|
||
|
|
<li
|
||
|
|
key={`${offer.vendor}-${offer.url}-${index}`}
|
||
|
|
className="p-3 rounded-lg bg-zinc-800/80 border border-zinc-700"
|
||
|
|
>
|
||
|
|
<div className="flex items-start justify-between gap-3">
|
||
|
|
<div className="min-w-0 flex-1">
|
||
|
|
<div className="flex items-center gap-2 flex-wrap">
|
||
|
|
<span className="text-sm font-medium text-white truncate">
|
||
|
|
{offer.vendor}
|
||
|
|
</span>
|
||
|
|
<span
|
||
|
|
className={`text-[10px] px-1.5 py-0.5 rounded border ${CONFIDENCE_STYLES[offer.confidence]}`}
|
||
|
|
>
|
||
|
|
{CONFIDENCE_LABELS[offer.confidence]}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<p className="text-xs text-zinc-400 mt-0.5 line-clamp-2">{offer.title}</p>
|
||
|
|
{offer.shippingNote && (
|
||
|
|
<p className="text-[11px] text-zinc-500 mt-1">{offer.shippingNote}</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<div className="flex flex-col items-end gap-2 flex-shrink-0">
|
||
|
|
<span className="text-sm font-semibold text-green-400">
|
||
|
|
{formatPrice(offer.price, offer.currency)}
|
||
|
|
</span>
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<a
|
||
|
|
href={offer.url}
|
||
|
|
target="_blank"
|
||
|
|
rel="noopener noreferrer"
|
||
|
|
className="text-[11px] text-blue-400 hover:text-blue-300 transition-colors"
|
||
|
|
>
|
||
|
|
Apri
|
||
|
|
</a>
|
||
|
|
{offer.price !== null && (
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={() => onApplyPrice(offer.price as number)}
|
||
|
|
className="text-[11px] px-2 py-1 rounded bg-blue-600 text-white hover:bg-blue-500 transition-colors"
|
||
|
|
>
|
||
|
|
Applica
|
||
|
|
</button>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<p className="text-[11px] text-zinc-500 leading-relaxed">{result.disclaimer}</p>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|