makearmy-app/components/buying-guide/BuyingGuideList.tsx

191 lines
7.4 KiB
TypeScript
Raw Normal View History

"use client";
import { useEffect, useMemo, useState } from "react";
import { dxGet } from "./dx";
type Cat = { id: string | number; name: string };
type Sub = { id: string | number; name: string; bg_cat_id: string | number };
type Entry = {
submission_id: string | number;
title?: string | null;
brand?: string | null;
model?: string | null;
thumb?: { id: string; filename_disk?: string } | null;
bg_cat_id?: { id: string | number; name?: string } | string | number | null;
bg_sub_cat_id?: { id: string | number; name?: string } | string | number | null;
price_min?: number | null;
price_max?: number | null;
};
function assetUrl(idOrPath?: string) {
// Prefer Directus asset URL if you expose one publicly; fallback to /api/dx/assets/:id
if (!idOrPath) return "";
if (/^https?:\/\//i.test(idOrPath)) return idOrPath;
return `/api/dx/assets/${idOrPath}`;
}
export default function BuyingGuideList({ embedded = true }: { embedded?: boolean }) {
const [cats, setCats] = useState<Cat[]>([]);
const [subs, setSubs] = useState<Sub[]>([]);
const [entries, setEntries] = useState<Entry[]>([]);
const [loading, setLoading] = useState(true);
const [catId, setCatId] = useState<string | number | "all">("all");
const [subId, setSubId] = useState<string | number | "all">("all");
const [q, setQ] = useState("");
useEffect(() => {
(async () => {
setLoading(true);
try {
const [catRes, subRes] = await Promise.all([
dxGet<Cat[]>("items/bg_cat", { fields: "id,name", limit: 500, sort: "name" }),
dxGet<Sub[]>("items/bg_sub_cat", { fields: "id,name,bg_cat_id", limit: 1000, sort: "name" }),
]);
setCats(catRes);
setSubs(subRes);
} finally {
setLoading(false);
}
})();
}, []);
useEffect(() => {
(async () => {
setLoading(true);
try {
// Build filter
const filter: any = {};
if (catId !== "all") filter.bg_cat_id = { _eq: catId };
if (subId !== "all") filter.bg_sub_cat_id = { _eq: subId };
if (q.trim()) {
filter._or = [
{ title: { _icontains: q } },
{ brand: { _icontains: q } },
{ model: { _icontains: q } },
];
}
const data = await dxGet<Entry[]>("items/bg_entries", {
fields: [
"submission_id",
"title",
"brand",
"model",
"price_min",
"price_max",
"thumb.id",
"bg_cat_id.id",
"bg_cat_id.name",
"bg_sub_cat_id.id",
"bg_sub_cat_id.name",
].join(","),
filter: JSON.stringify(filter),
limit: 48,
sort: "brand,model,title",
});
setEntries(data);
} finally {
setLoading(false);
}
})();
}, [catId, subId, q]);
const subsForCat = useMemo(
() => (catId === "all" ? subs : subs.filter(s => String(s.bg_cat_id) === String(catId))),
[subs, catId]
);
return (
<div className="space-y-4">
{/* Controls */}
<div className="flex flex-wrap gap-3 items-end">
<div className="flex flex-col">
<label className="text-xs text-zinc-400 mb-1">Category</label>
<select
className="rounded-md border bg-background px-2 py-1"
value={catId}
onChange={e => { setCatId(e.target.value === "all" ? "all" : e.target.value); setSubId("all"); }}
>
<option value="all">All</option>
{cats.map(c => <option key={c.id} value={String(c.id)}>{c.name}</option>)}
</select>
</div>
<div className="flex flex-col">
<label className="text-xs text-zinc-400 mb-1">Subcategory</label>
<select
className="rounded-md border bg-background px-2 py-1"
value={subId}
onChange={e => setSubId(e.target.value === "all" ? "all" : e.target.value)}
>
<option value="all">All</option>
{subsForCat.map(s => <option key={s.id} value={String(s.id)}>{s.name}</option>)}
</select>
</div>
<div className="flex-1 min-w-[220px]">
<label className="text-xs text-zinc-400 mb-1 block">Search</label>
<input
className="w-full rounded-md border bg-background px-3 py-1.5"
placeholder="brand, model, title…"
value={q}
onChange={e => setQ(e.target.value)}
/>
</div>
</div>
{/* Results */}
{loading ? (
<div className="text-sm text-zinc-400">Loading</div>
) : entries.length === 0 ? (
<div className="text-sm text-zinc-400">No results.</div>
) : (
<div className="grid gap-3 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{entries.map(e => {
const thumbId = (e.thumb as any)?.id as string | undefined;
return (
<a
key={e.submission_id}
href={`/buying-guide/${e.submission_id}`}
className="rounded-lg border hover:bg-muted/40 p-3 block"
target="_blank"
rel="noopener noreferrer"
title="Open product in new tab"
>
<div className="aspect-[4/3] rounded-md border overflow-hidden mb-2 grid place-items-center bg-muted">
{thumbId ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={assetUrl(thumbId)}
alt=""
className="object-cover w-full h-full"
loading="lazy"
decoding="async"
/>
) : (
<div className="text-xs text-zinc-500">No image</div>
)}
</div>
<div className="font-medium truncate">
{e.brand ? `${e.brand} ` : ""}{e.model || e.title || "Untitled"}
</div>
<div className="text-xs text-zinc-400 truncate">
{(e.bg_cat_id as any)?.name || ""}{(e.bg_sub_cat_id as any)?.name ? `${(e.bg_sub_cat_id as any).name}` : ""}
</div>
{(e.price_min || e.price_max) && (
<div className="text-sm mt-1">
{e.price_min && e.price_max && e.price_min !== e.price_max
? `$${e.price_min.toLocaleString()}$${e.price_max.toLocaleString()}`
: `$${(e.price_min || e.price_max)!.toLocaleString()}`}
</div>
)}
</a>
);
})}
</div>
)}
</div>
);
}