188 lines
6.3 KiB
TypeScript
188 lines
6.3 KiB
TypeScript
// app/buying-guide/product/[id]/page.tsx
|
||
import Link from "next/link";
|
||
import ReactMarkdown from "react-markdown";
|
||
|
||
const API_URL = process.env.NEXT_PUBLIC_API_BASE_URL;
|
||
const ASSET_URL = process.env.NEXT_PUBLIC_ASSET_URL;
|
||
|
||
async function getEntry(id: string) {
|
||
const res = await fetch(
|
||
`${API_URL}/items/bg_entries/${id}?fields=*,links.id,links.text,links.url,links.target,scores.id,scores.cat,scores.value,scores.body,header.id,date_updated`,
|
||
{
|
||
cache: "no-store",
|
||
}
|
||
);
|
||
|
||
if (!res.ok) {
|
||
const error = await res.text();
|
||
console.error(`Failed to fetch entry: ${error}`);
|
||
throw new Error(`Error fetching entry ${id}`);
|
||
}
|
||
|
||
const { data } = await res.json();
|
||
return data;
|
||
}
|
||
|
||
export default async function ProductDetail({
|
||
params,
|
||
}: {
|
||
params: Promise<{ id: string }>;
|
||
}) {
|
||
const id = (await params).id;
|
||
const entry = await getEntry(id);
|
||
|
||
const avgScore =
|
||
entry?.scores?.length > 0
|
||
? (
|
||
entry.scores.reduce((sum: number, s: any) => sum + Number(s.value), 0) /
|
||
entry.scores.length
|
||
).toFixed(1)
|
||
: "N/A";
|
||
|
||
const headerUrl = entry.header?.id
|
||
? `${ASSET_URL}/assets/${entry.header.id}?cache-buster=${entry.date_updated}&key=system-large-contain`
|
||
: null;
|
||
|
||
return (
|
||
<div className="max-w-4xl mx-auto px-4 py-8 space-y-6">
|
||
{/* Header Banner */}
|
||
{headerUrl && (
|
||
<div className="w-full h-64 relative overflow-hidden rounded-xl shadow">
|
||
<img
|
||
src={headerUrl}
|
||
alt="Header Image"
|
||
className="object-cover w-full h-full rounded-xl"
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Title */}
|
||
<div className="flex justify-between items-start">
|
||
<div>
|
||
<h2 className="text-xl font-semibold text-white">{entry.product_make}</h2>
|
||
<h1 className="text-4xl font-bold text-yellow-500 mt-2">{entry.product_model}</h1>
|
||
</div>
|
||
<div className="text-right">
|
||
{entry.product_price && (
|
||
<p className="text-lg text-white font-medium mt-1">
|
||
{entry.product_price.startsWith("Starting at") ? entry.product_price : `Starting at ${entry.product_price}`}
|
||
</p>
|
||
)}
|
||
<Link
|
||
href="/buying-guide"
|
||
className="text-sm text-blue-500 underline mt-2 inline-block"
|
||
>
|
||
← Back to Buying Guide
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Links & Score Summary */}
|
||
{(Array.isArray(entry.links) || Array.isArray(entry.scores)) && (
|
||
<div className="flex flex-col md:flex-row gap-8">
|
||
{Array.isArray(entry.links) && entry.links.length > 0 && (
|
||
<div className="md:w-1/2">
|
||
<h3 className="text-xl font-semibold mb-2">Links</h3>
|
||
<ul className="list-disc ml-6 space-y-1">
|
||
{entry.links.map((link: any, idx: number) => (
|
||
<li key={idx}>
|
||
<a
|
||
href={link.url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="text-blue-700 underline"
|
||
>
|
||
{link.text || link.url}
|
||
</a>
|
||
{link.target && (
|
||
<span className="text-sm text-gray-500"> ({link.target})</span>
|
||
)}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
)}
|
||
|
||
{(Array.isArray(entry.scores) && entry.scores.length > 0) && (
|
||
<div className="md:w-1/2">
|
||
<h3 className="text-xl font-semibold mb-2">Score Summary</h3>
|
||
<ul className="space-y-1">
|
||
{entry.scores.map((s: any, idx: number) => (
|
||
<li key={idx} className="flex justify-between">
|
||
<span>{s.cat}</span>
|
||
<span className="font-semibold">{s.value}/10</span>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
<p className="mt-2 font-bold text-right">Total: {avgScore}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Overview */}
|
||
{entry.review_overview_text && (
|
||
<div className="prose max-w-none">
|
||
<h3 className="text-xl font-semibold mb-2">Overview</h3>
|
||
<ReactMarkdown>{entry.review_overview_text}</ReactMarkdown>
|
||
</div>
|
||
)}
|
||
|
||
{/* Intro */}
|
||
{entry.review_intro_text && (
|
||
<div className="prose max-w-none">
|
||
<h3 className="text-xl font-semibold mb-2">{`${entry.product_make}, ${entry.product_model} Review by ${entry.author}`}</h3>
|
||
<ReactMarkdown>{entry.review_intro_text}</ReactMarkdown>
|
||
</div>
|
||
)}
|
||
|
||
{/* Detailed Scores */}
|
||
{(Array.isArray(entry.scores) && entry.scores.length > 0) && (
|
||
<div className="space-y-4">
|
||
{entry.scores.map((s: any, idx: number) => (
|
||
<div key={idx} className="p-4 rounded border">
|
||
<p className="text-xl font-semibold">
|
||
{s.cat} – <span className="text-blue-600">{s.value}/10</span>
|
||
</p>
|
||
<div className="text-sm text-gray-400">
|
||
<ReactMarkdown>{s.body}</ReactMarkdown>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Recommendation */}
|
||
{entry.rec_text && (
|
||
<div className="prose max-w-none">
|
||
<h3 className="text-xl font-semibold mb-2">Recommendation</h3>
|
||
<ReactMarkdown>{entry.rec_text}</ReactMarkdown>
|
||
</div>
|
||
)}
|
||
|
||
{/* Updates */}
|
||
{entry.updates && (
|
||
<div className="prose max-w-none">
|
||
<h3 className="text-xl font-semibold mb-2">Updates</h3>
|
||
<ReactMarkdown>{entry.updates}</ReactMarkdown>
|
||
</div>
|
||
)}
|
||
|
||
{/* Video */}
|
||
{entry.video_review_url && (
|
||
<div>
|
||
<h3 className="text-xl font-semibold mb-2">Video Review</h3>
|
||
<div className="aspect-w-16 aspect-h-9">
|
||
<iframe
|
||
src={entry.video_review_url.replace("watch?v=", "embed/")}
|
||
className="w-full h-96 rounded"
|
||
frameBorder="0"
|
||
allowFullScreen
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|