From 73bba240abc81293dfbb4a953c6227356dc8edb6 Mon Sep 17 00:00:00 2001 From: makearmy Date: Wed, 15 Oct 2025 16:50:32 -0400 Subject: [PATCH] buyingguide test fix --- components/buying-guide/BuyingGuideList.tsx | 474 +++++++++++++------- 1 file changed, 313 insertions(+), 161 deletions(-) diff --git a/components/buying-guide/BuyingGuideList.tsx b/components/buying-guide/BuyingGuideList.tsx index 3bd77c45..48ab3782 100644 --- a/components/buying-guide/BuyingGuideList.tsx +++ b/components/buying-guide/BuyingGuideList.tsx @@ -1,190 +1,342 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; -import { dxGet } from "./dx"; +import { useEffect, useState, useMemo } from "react"; +import { useSearchParams } from "next/navigation"; +import Link from "next/link"; +import Image from "next/image"; -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}`; +interface Entry { + id: number; + product_make: string; + product_model: string; + product_price?: string; + review_overview_text?: string; + bg_entry_sub_cat?: number; + bg_entry_cat?: number; + index?: { + id: string; + filename_disk?: string; + type?: string; + }; + header?: { + id: string; + filename_disk?: string; + type?: string; + }; } -export default function BuyingGuideList({ embedded = true }: { embedded?: boolean }) { - const [cats, setCats] = useState([]); - const [subs, setSubs] = useState([]); +interface SubCategory { + id: number; + name: string; + bg_entry_cat?: number; +} + +interface Category { + id: number; + name: string; +} + +export default function BuyingGuidePage() { + const searchParams = useSearchParams(); + const initialQuery = searchParams.get("query") || ""; + + const [query, setQuery] = useState(initialQuery); + const [debouncedQuery, setDebouncedQuery] = useState(initialQuery); const [entries, setEntries] = useState([]); + const [categories, setCategories] = useState([]); + const [subcategories, setSubcategories] = useState([]); + const [selectedCat, setSelectedCat] = useState(""); + const [selectedSubCat, setSelectedSubCat] = useState(""); const [loading, setLoading] = useState(true); - const [catId, setCatId] = useState("all"); - const [subId, setSubId] = useState("all"); - const [q, setQ] = useState(""); useEffect(() => { - (async () => { - setLoading(true); + const timer = setTimeout(() => setDebouncedQuery(query), 300); + return () => clearTimeout(timer); + }, [query]); + + useEffect(() => { + const fetchData = async () => { try { - const [catRes, subRes] = await Promise.all([ - dxGet("items/bg_cat", { fields: "id,name", limit: 500, sort: "name" }), - dxGet("items/bg_sub_cat", { fields: "id,name,bg_cat_id", limit: 1000, sort: "name" }), + const [entriesRes, catRes, subCatRes] = await Promise.all([ + fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/bg_entries?fields=id,index.id,index.filename_disk,index.type,header.id,header.filename_disk,product_make,product_model,product_price,review_overview_text,bg_entry_cat,bg_entry_sub_cat&limit=-1&sort[]=sort`), + fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/bg_cat?fields=id,name&limit=-1`), + fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/bg_sub_cat?fields=id,name,bg_entry_cat&limit=-1`), ]); - setCats(catRes); - setSubs(subRes); - } finally { + + const [entriesData, catData, subCatData] = await Promise.all([ + entriesRes.json(), + catRes.json(), + subCatRes.json(), + ]); + + setEntries(entriesData?.data || []); + setCategories(catData?.data || []); + setSubcategories(subCatData?.data || []); + setLoading(false); + } catch (err) { + console.error("Error fetching data:", err); setLoading(false); } - })(); + }; + + fetchData(); }, []); - 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 normalize = (str: string) => str?.toLowerCase().replace(/[_\s]/g, ""); - const data = await dxGet("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", - }); + const filtered = useMemo(() => { + const q = normalize(debouncedQuery); + return entries.filter((entry) => { + const catMatch = selectedCat ? entry.bg_entry_cat === parseInt(selectedCat) : true; + const subCatMatch = selectedSubCat ? entry.bg_entry_sub_cat === parseInt(selectedSubCat) : true; + const searchMatch = q + ? [entry.product_make, entry.product_model, entry.review_overview_text].some((field) => + normalize(field || "").includes(q) + ) + : true; + return catMatch && subCatMatch && searchMatch; + }); + }, [entries, debouncedQuery, selectedCat, selectedSubCat]); - setEntries(data); - } finally { - setLoading(false); - } - })(); - }, [catId, subId, q]); + const filteredSubcategories = useMemo(() => { + return selectedCat + ? subcategories.filter((sub) => sub.bg_entry_cat === parseInt(selectedCat)) + : subcategories; + }, [subcategories, selectedCat]); - const subsForCat = useMemo( - () => (catId === "all" ? subs : subs.filter(s => String(s.bg_cat_id) === String(catId))), - [subs, catId] - ); + const featuredEntry = useMemo(() => { + if (!entries.length) return null; + const randomIndex = Math.floor(Math.random() * entries.length); + return entries[randomIndex]; + }, [entries]); + + const secondFeaturedEntry = useMemo(() => { + if (entries.length < 2) return null; + let secondIndex = Math.floor(Math.random() * entries.length); + while (entries[secondIndex].id === featuredEntry?.id) { + secondIndex = Math.floor(Math.random() * entries.length); + } + return entries[secondIndex]; + }, [entries, featuredEntry]); return ( -
- {/* Controls */} -
-
- - -
+
+ -
- - -
+
+
+

Buying Guide

+ + + setQuery(e.target.value)} + placeholder="Search products by make, model, etc..." + className="w-full mb-4 dark:bg-background border border-border rounded-md p-2" + /> +

+ Discover reviewed laser products and accessories. +

+ + ← Back to Main Menu + +
-
- - setQ(e.target.value)} - /> -
-
- - {/* Results */} - {loading ? ( -
Loading…
- ) : entries.length === 0 ? ( -
No results.
- ) : ( -
- {entries.map(e => { - const thumbId = (e.thumb as any)?.id as string | undefined; - return ( - - + ) + ))} + +
+

Popular Categories

+
    + {categories.slice(0, 5).map((cat) => ( +
  • + +
  • + ))} +
+
+ +
+

Recently Added

+
    + {entries.slice(0, 3).map((e) => ( +
  • + + {e.product_make} {e.product_model} + +
  • + ))} +
+
+ +
+

What Is This?

+

+ This Buying Guide helps you compare laser-related gear with hands-on reviews, scores, and recommendations. Use the filters and search to find what you’re looking for! +

+
+
+ +
+ + {loading ? ( +

Loading entries...

+ ) : filtered.length === 0 ? ( +

No entries found.

+ ) : ( +
+ {filtered.map((entry) => { + const filename = entry.index?.filename_disk; + return ( +
+ {filename ? ( + {`${entry.product_make} + ) : ( +
+ No Image +
+ )} +
+
+

+ {entry.product_make} +

+ + {entry.product_model} + + {entry.product_price !== undefined && ( +

+ Starting at {entry.product_price} +

+ )} +

+ {entry.review_overview_text?.slice(0, 120)}... +

+
+
+
+ ); + })} +
+ )}
- )} -
); } +