completely refactored utilities for direct rendering, killed iframes
This commit is contained in:
parent
12dd2c6c06
commit
f08a7456ee
37 changed files with 1824 additions and 1350 deletions
190
components/buying-guide/BuyingGuideList.tsx
Normal file
190
components/buying-guide/BuyingGuideList.tsx
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
"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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue