feat: initial public release (MAESTRO)
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import type { AmazonData } from './types';
|
||||
|
||||
function StarRating({ rating, reviewCount }: { rating: number; reviewCount?: number }) {
|
||||
const full = Math.floor(rating);
|
||||
const half = rating - full >= 0.5;
|
||||
const stars: string[] = [];
|
||||
for (let i = 0; i < full; i++) stars.push('\u2605');
|
||||
if (half) stars.push('\u2606');
|
||||
return (
|
||||
<span className="text-amber-400 text-sm">
|
||||
{stars.join('')} {rating.toFixed(1)}
|
||||
{reviewCount != null && <span className="text-slate-400 text-xs ml-1">({reviewCount.toLocaleString()}件)</span>}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function AmazonProductsDetail({ data }: { data: AmazonData }) {
|
||||
const { query, products } = data;
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h2 className="text-lg font-bold text-slate-800 mb-4">
|
||||
🛒 Amazon 検索結果: 「{query}」
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
{products.map((p, i) => (
|
||||
<div key={p.asin} className="bg-white border border-slate-200 rounded-xl p-4">
|
||||
<div className="flex gap-4 flex-col sm:flex-row">
|
||||
{/* Product image */}
|
||||
<div className="w-full sm:w-40 h-40 bg-slate-50 rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden">
|
||||
{p.imageUrl ? (
|
||||
<img src={p.imageUrl} alt={p.title} className="max-h-full max-w-full object-contain" />
|
||||
) : (
|
||||
<span className="text-4xl">💾</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Product info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-slate-400 mb-1" style={{ fontSize: 13 }}>#{i + 1}</div>
|
||||
<h3 className="text-sm font-semibold text-slate-800 leading-snug mb-2">{p.title}</h3>
|
||||
|
||||
{p.price && (
|
||||
<div className="text-xl font-bold text-red-600 mb-1">{p.price}</div>
|
||||
)}
|
||||
|
||||
{p.rating != null && (
|
||||
<div className="mb-2">
|
||||
<StarRating rating={p.rating} reviewCount={p.reviewCount} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-xs text-slate-400 mb-3">ASIN: {p.asin}</div>
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<a
|
||||
href={p.productUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 bg-amber-400 hover:bg-amber-500 text-slate-900 text-xs font-semibold rounded-lg no-underline transition-colors"
|
||||
>
|
||||
Amazon で見る
|
||||
</a>
|
||||
<a
|
||||
href={p.keepaDetailUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 bg-slate-100 hover:bg-slate-200 text-slate-700 text-xs font-semibold rounded-lg no-underline transition-colors"
|
||||
>
|
||||
Keepa で見る
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Keepa price graph */}
|
||||
<div className="mt-4 bg-slate-50 rounded-lg p-3">
|
||||
<div className="text-xs text-slate-500 mb-2">📈 価格推移 (Keepa)</div>
|
||||
<img
|
||||
src={p.keepaGraphUrl}
|
||||
alt={`${p.title} 価格推移`}
|
||||
className="w-full rounded"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user