background remover fix
This commit is contained in:
parent
da4d0620a4
commit
22bc048475
3 changed files with 302 additions and 55 deletions
|
|
@ -6,7 +6,7 @@ import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
interface Entry {
|
interface Entry {
|
||||||
id: number;
|
id: number | string;
|
||||||
product_make: string;
|
product_make: string;
|
||||||
product_model: string;
|
product_model: string;
|
||||||
product_price?: string;
|
product_price?: string;
|
||||||
|
|
@ -58,8 +58,10 @@ export default function BuyingGuidePage() {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const [entriesRes, catRes, subCatRes] = await Promise.all([
|
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(
|
||||||
fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/bg_cat?fields=id,name&limit=-1`),
|
`${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`),
|
fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/bg_sub_cat?fields=id,name,bg_entry_cat&limit=-1`),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -99,9 +101,7 @@ export default function BuyingGuidePage() {
|
||||||
}, [entries, debouncedQuery, selectedCat, selectedSubCat]);
|
}, [entries, debouncedQuery, selectedCat, selectedSubCat]);
|
||||||
|
|
||||||
const filteredSubcategories = useMemo(() => {
|
const filteredSubcategories = useMemo(() => {
|
||||||
return selectedCat
|
return selectedCat ? subcategories.filter((sub) => sub.bg_entry_cat === parseInt(selectedCat)) : subcategories;
|
||||||
? subcategories.filter((sub) => sub.bg_entry_cat === parseInt(selectedCat))
|
|
||||||
: subcategories;
|
|
||||||
}, [subcategories, selectedCat]);
|
}, [subcategories, selectedCat]);
|
||||||
|
|
||||||
const featuredEntry = useMemo(() => {
|
const featuredEntry = useMemo(() => {
|
||||||
|
|
@ -119,6 +119,13 @@ export default function BuyingGuidePage() {
|
||||||
return entries[secondIndex];
|
return entries[secondIndex];
|
||||||
}, [entries, featuredEntry]);
|
}, [entries, featuredEntry]);
|
||||||
|
|
||||||
|
// Build a URL that sets ?product=<id> while preserving any existing params.
|
||||||
|
const makeProductHref = (id: number | string) => {
|
||||||
|
const sp = new URLSearchParams(Array.from(searchParams.entries()));
|
||||||
|
sp.set("product", String(id));
|
||||||
|
return `?${sp.toString()}`;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 max-w-7xl mx-auto">
|
<div className="p-6 max-w-7xl mx-auto">
|
||||||
<style jsx global>{`
|
<style jsx global>{`
|
||||||
|
|
@ -198,18 +205,14 @@ export default function BuyingGuidePage() {
|
||||||
placeholder="Search products by make, model, etc..."
|
placeholder="Search products by make, model, etc..."
|
||||||
className="w-full mb-4 dark:bg-background border border-border rounded-md p-2"
|
className="w-full mb-4 dark:bg-background border border-border rounded-md p-2"
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-muted-foreground mb-2">
|
<p className="text-sm text-muted-foreground mb-2">Discover reviewed laser products and accessories.</p>
|
||||||
Discover reviewed laser products and accessories.
|
<a href="/" className="inline-block mt-2 px-4 py-2 bg-accent text-background rounded-md text-sm">
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
href="/"
|
|
||||||
className="inline-block mt-2 px-4 py-2 bg-accent text-background rounded-md text-sm"
|
|
||||||
>
|
|
||||||
← Back to Main Menu
|
← Back to Main Menu
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{[featuredEntry, secondFeaturedEntry].map((entry, idx) => (
|
{[featuredEntry, secondFeaturedEntry].map(
|
||||||
|
(entry, idx) =>
|
||||||
entry && (
|
entry && (
|
||||||
<div key={idx} className="card bg-card text-card-foreground p-4">
|
<div key={idx} className="card bg-card text-card-foreground p-4">
|
||||||
<h2 className="text-md font-semibold mb-2">Featured Product</h2>
|
<h2 className="text-md font-semibold mb-2">Featured Product</h2>
|
||||||
|
|
@ -227,21 +230,14 @@ export default function BuyingGuidePage() {
|
||||||
No Header
|
No Header
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Link
|
<Link href={makeProductHref(entry.id)} className="text-accent font-semibold text-lg hover:underline">
|
||||||
href={`/buying-guide/product/${entry.id}`}
|
|
||||||
className="text-accent font-semibold text-lg hover:underline"
|
|
||||||
>
|
|
||||||
{entry.product_make} {entry.product_model}
|
{entry.product_make} {entry.product_model}
|
||||||
</Link>
|
</Link>
|
||||||
{entry.product_price && (
|
{entry.product_price && <p className="text-sm text-white">Starting at {entry.product_price}</p>}
|
||||||
<p className="text-sm text-white">Starting at {entry.product_price}</p>
|
<p className="text-sm text-muted-foreground mt-1">{entry.review_overview_text?.slice(0, 140)}...</p>
|
||||||
)}
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
|
||||||
{entry.review_overview_text?.slice(0, 140)}...
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
))}
|
)}
|
||||||
|
|
||||||
<div className="card bg-card text-card-foreground p-4">
|
<div className="card bg-card text-card-foreground p-4">
|
||||||
<h2 className="text-md font-semibold mb-2">Popular Categories</h2>
|
<h2 className="text-md font-semibold mb-2">Popular Categories</h2>
|
||||||
|
|
@ -267,7 +263,7 @@ export default function BuyingGuidePage() {
|
||||||
<ul className="text-sm space-y-1">
|
<ul className="text-sm space-y-1">
|
||||||
{entries.slice(0, 3).map((e) => (
|
{entries.slice(0, 3).map((e) => (
|
||||||
<li key={e.id}>
|
<li key={e.id}>
|
||||||
<Link href={`/buying-guide/product/${e.id}`} className="text-accent hover:underline">
|
<Link href={makeProductHref(e.id)} className="text-accent hover:underline">
|
||||||
{e.product_make} {e.product_model}
|
{e.product_make} {e.product_model}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -278,7 +274,8 @@ export default function BuyingGuidePage() {
|
||||||
<div className="card bg-card text-card-foreground p-4">
|
<div className="card bg-card text-card-foreground p-4">
|
||||||
<h2 className="text-md font-semibold mb-2">What Is This?</h2>
|
<h2 className="text-md font-semibold mb-2">What Is This?</h2>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
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!
|
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!
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -305,26 +302,20 @@ export default function BuyingGuidePage() {
|
||||||
unoptimized
|
unoptimized
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="entry-image bg-zinc-800 flex items-center justify-center text-zinc-400">
|
<div className="entry-image bg-zinc-800 flex items-center justify-center text-zinc-400">No Image</div>
|
||||||
No Image
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<div className="entry-content">
|
<div className="entry-content">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-muted-foreground truncate-title">
|
<p className="text-sm font-medium text-muted-foreground truncate-title">{entry.product_make}</p>
|
||||||
{entry.product_make}
|
|
||||||
</p>
|
|
||||||
<Link
|
<Link
|
||||||
href={`/buying-guide/product/${entry.id}`}
|
href={makeProductHref(entry.id)}
|
||||||
className="text-lg font-semibold text-accent underline truncate-title"
|
className="text-lg font-semibold text-accent underline truncate-title"
|
||||||
title={entry.product_model}
|
title={entry.product_model}
|
||||||
>
|
>
|
||||||
{entry.product_model}
|
{entry.product_model}
|
||||||
</Link>
|
</Link>
|
||||||
{entry.product_price !== undefined && (
|
{entry.product_price !== undefined && (
|
||||||
<p className="text-sm text-foreground mt-1 font-medium">
|
<p className="text-sm text-foreground mt-1 font-medium">Starting at {entry.product_price}</p>
|
||||||
Starting at {entry.product_price}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
{entry.review_overview_text?.slice(0, 120)}...
|
{entry.review_overview_text?.slice(0, 120)}...
|
||||||
|
|
@ -339,4 +330,3 @@ export default function BuyingGuidePage() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
246
components/buying-guide/BuyingGuideProductClient.tsx
Normal file
246
components/buying-guide/BuyingGuideProductClient.tsx
Normal file
|
|
@ -0,0 +1,246 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo, useEffect, useState } from "react";
|
||||||
|
import { useSearchParams, useRouter } from "next/navigation";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
|
||||||
|
const API_URL = process.env.NEXT_PUBLIC_API_BASE_URL!;
|
||||||
|
const ASSET_URL = process.env.NEXT_PUBLIC_ASSET_URL!;
|
||||||
|
|
||||||
|
type Score = { id: string | number; cat: string; value: number | string; body?: string };
|
||||||
|
type LinkItem = { id?: string | number; text?: string; url: string; target?: string };
|
||||||
|
|
||||||
|
type Entry = {
|
||||||
|
id: number | string;
|
||||||
|
product_make?: string;
|
||||||
|
product_model?: string;
|
||||||
|
product_price?: string;
|
||||||
|
review_overview_text?: string;
|
||||||
|
review_intro_text?: string;
|
||||||
|
author?: string;
|
||||||
|
rec_text?: string;
|
||||||
|
updates?: string;
|
||||||
|
video_review_url?: string;
|
||||||
|
header?: { id?: string };
|
||||||
|
date_updated?: string | number;
|
||||||
|
links?: LinkItem[];
|
||||||
|
scores?: Score[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function BuyingGuideProductClient() {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const router = useRouter();
|
||||||
|
const productId = searchParams.get("product");
|
||||||
|
|
||||||
|
const [entry, setEntry] = useState<Entry | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// Fetch the entry when productId changes
|
||||||
|
useEffect(() => {
|
||||||
|
let abort = false;
|
||||||
|
async function run() {
|
||||||
|
if (!productId) return;
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`${API_URL}/items/bg_entries/${productId}?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) throw new Error(await res.text());
|
||||||
|
const { data } = await res.json();
|
||||||
|
if (!abort) setEntry(data);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (!abort) setError(e?.message || "Failed to load product");
|
||||||
|
} finally {
|
||||||
|
if (!abort) setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run();
|
||||||
|
return () => {
|
||||||
|
abort = true;
|
||||||
|
};
|
||||||
|
}, [productId]);
|
||||||
|
|
||||||
|
const avgScore = useMemo(() => {
|
||||||
|
if (!entry?.scores?.length) return "N/A";
|
||||||
|
const sum = entry.scores.reduce((s, it) => s + Number(it.value ?? 0), 0);
|
||||||
|
return (sum / entry.scores.length).toFixed(1);
|
||||||
|
}, [entry]);
|
||||||
|
|
||||||
|
const headerUrl =
|
||||||
|
entry?.header?.id
|
||||||
|
? `${ASSET_URL}/assets/${entry.header.id}?cache-buster=${entry.date_updated}&key=system-large-contain`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!productId) return null; // switcher guards this, but be defensive
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="max-w-4xl mx-auto px-4 py-8">Loading…</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||||
|
<p className="text-red-500 text-sm">Error: {error}</p>
|
||||||
|
<button
|
||||||
|
className="text-blue-500 underline mt-2"
|
||||||
|
onClick={() => {
|
||||||
|
const sp = new URLSearchParams(Array.from(searchParams.entries()));
|
||||||
|
sp.delete("product");
|
||||||
|
router.push(`?${sp.toString()}`, { scroll: false });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
← Back to Buying Guide
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entry) return 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>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const sp = new URLSearchParams(Array.from(searchParams.entries()));
|
||||||
|
sp.delete("product");
|
||||||
|
router.push(`?${sp.toString()}`, { scroll: false });
|
||||||
|
}}
|
||||||
|
className="text-sm text-blue-500 underline mt-2 inline-block"
|
||||||
|
>
|
||||||
|
← Back to Buying Guide
|
||||||
|
</button>
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,6 @@ import { useSearchParams, useRouter } from "next/navigation";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
// these should already exist under components/buying-guide/*
|
|
||||||
const BuyingGuideList = dynamic(
|
const BuyingGuideList = dynamic(
|
||||||
() => import("@/components/buying-guide/BuyingGuideList"),
|
() => import("@/components/buying-guide/BuyingGuideList"),
|
||||||
{ ssr: false }
|
{ ssr: false }
|
||||||
|
|
@ -14,36 +13,41 @@ const LaserFinderPanel = dynamic(
|
||||||
() => import("@/components/buying-guide/LaserFinderPanel"),
|
() => import("@/components/buying-guide/LaserFinderPanel"),
|
||||||
{ ssr: false }
|
{ ssr: false }
|
||||||
);
|
);
|
||||||
|
const BuyingGuideProductClient = dynamic(
|
||||||
|
() => import("@/components/buying-guide/BuyingGuideProductClient"),
|
||||||
|
{ ssr: false }
|
||||||
|
);
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ key: "list", label: "Guide" },
|
{ key: "list", label: "Buying Guide" },
|
||||||
{ key: "finder", label: "Laser Finder" },
|
{ key: "finder", label: "Laser Finder" },
|
||||||
] as const;
|
];
|
||||||
|
|
||||||
export default function BuyingGuideSwitcher() {
|
export default function BuyingGuideSwitcher() {
|
||||||
const sp = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const active = useMemo(() => {
|
const active = useMemo(() => searchParams.get("tab") || "list", [searchParams]);
|
||||||
const t = (sp.get("bg") || TABS[0].key).toLowerCase();
|
const productId = searchParams.get("product");
|
||||||
return TABS.some(x => x.key === t) ? t : TABS[0].key;
|
|
||||||
}, [sp]);
|
|
||||||
|
|
||||||
function setTab(k: string) {
|
const setTab = (key: string) => {
|
||||||
const q = new URLSearchParams(sp.toString());
|
const sp = new URLSearchParams(Array.from(searchParams.entries()));
|
||||||
q.set("bg", k);
|
sp.set("tab", key);
|
||||||
router.replace(`/portal/buying-guide?${q.toString()}`, { scroll: false });
|
// When changing tabs, ensure product detail (if open) is cleared
|
||||||
}
|
sp.delete("product");
|
||||||
|
router.push(`?${sp.toString()}`, { scroll: false });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex gap-2">
|
{/* Tabs */}
|
||||||
{TABS.map(t => (
|
<div className="inline-flex rounded-md border overflow-hidden">
|
||||||
|
{TABS.map((t) => (
|
||||||
<button
|
<button
|
||||||
key={t.key}
|
key={t.key}
|
||||||
onClick={() => setTab(t.key)}
|
onClick={() => setTab(t.key)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-md border px-3 py-1.5 text-sm",
|
"px-3 py-2 text-sm transition-colors",
|
||||||
active === t.key ? "bg-primary text-primary-foreground" : "hover:bg-muted"
|
active === t.key ? "bg-primary text-primary-foreground" : "hover:bg-muted"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -52,8 +56,15 @@ export default function BuyingGuideSwitcher() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
<div className="rounded-md border p-4">
|
<div className="rounded-md border p-4">
|
||||||
{active === "finder" ? <LaserFinderPanel /> : <BuyingGuideList />}
|
{productId ? (
|
||||||
|
<BuyingGuideProductClient />
|
||||||
|
) : active === "finder" ? (
|
||||||
|
<LaserFinderPanel />
|
||||||
|
) : (
|
||||||
|
<BuyingGuideList />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue