import { useState, useMemo, useEffect, useCallback } from "react"; import { Search } from "lucide-react"; import { toast } from "sonner"; import { fetchLuxsinAudioBrands, fetchLuxsinAudioModelList, fetchLuxsinAudioModels, type LuxsinAudioBrand, type LuxsinAudioModelListItem, type LuxsinAudioModel, } from "@/lib/luxsinApi"; import { CATALOG_TARGETS } from "../eqConstants"; import type { PeqEqUi } from "../types"; import { eqInterp } from "../eqFormatters"; type BrandDrawerTab = "brands" | "models" | "target"; export function BrandDrawer({ open, onClose, eqUi, onConfirm, }: { open: boolean; onClose: () => void; eqUi: PeqEqUi; onConfirm: (brand: string, name: string, target: string, form?: string) => Promise; }) { const [brandDrawerTab, setBrandDrawerTab] = useState("brands"); const [brandSearchQuery, setBrandSearchQuery] = useState(""); const [catalogBrands, setCatalogBrands] = useState([]); const [catalogBrandsLoading, setCatalogBrandsLoading] = useState(false); const [catalogBrandsError, setCatalogBrandsError] = useState(null); const [catalogSearchResults, setCatalogSearchResults] = useState([]); const [catalogSearchLoading, setCatalogSearchLoading] = useState(false); const [catalogSearchError, setCatalogSearchError] = useState(null); const [selectedCatalogBrand, setSelectedCatalogBrand] = useState(""); const [catalogModels, setCatalogModels] = useState([]); const [catalogModelsLoading, setCatalogModelsLoading] = useState(false); const [catalogModelsError, setCatalogModelsError] = useState(null); const [selectedCatalogModelFromSearch, setSelectedCatalogModelFromSearch] = useState(""); const [selectedCatalogModelName, setSelectedCatalogModelName] = useState(""); const [selectedCatalogModelForm, setSelectedCatalogModelForm] = useState(undefined); const [selectedCatalogTarget, setSelectedCatalogTarget] = useState(""); const [isConfirmingTarget, setIsConfirmingTarget] = useState(false); /* ── Derived data ── */ const filteredCatalogBrands = useMemo(() => { const q = brandSearchQuery.trim().toLowerCase(); if (!q) return catalogBrands; return catalogBrands.filter((b) => b.name.toLowerCase().includes(q)); }, [catalogBrands, brandSearchQuery]); const availableCatalogTargets = useMemo(() => { if (selectedCatalogModelForm === "in-ear") { return CATALOG_TARGETS.filter((item) => item.ear === "in" || item.ear === "all"); } if (selectedCatalogModelForm === "over-ear") { return CATALOG_TARGETS.filter((item) => item.ear === "over" || item.ear === "all"); } return CATALOG_TARGETS; }, [selectedCatalogModelForm]); /* ── API calls ── */ const loadCatalogBrands = useCallback(async () => { setCatalogBrandsLoading(true); setCatalogBrandsError(null); try { const list = await fetchLuxsinAudioBrands(); const sorted = [...list].sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" })); setCatalogBrands(sorted); } catch (e) { const msg = e instanceof Error ? e.message : "加载失败"; setCatalogBrandsError(msg); toast.error(eqUi.toastBrandsFail); } finally { setCatalogBrandsLoading(false); } }, [eqUi]); const searchCatalogByKeyword = useCallback(async (keyword: string) => { const q = keyword.trim(); if (!q) { setCatalogSearchResults([]); setCatalogSearchError(null); setCatalogSearchLoading(false); return; } setCatalogSearchLoading(true); setCatalogSearchError(null); try { const list = await fetchLuxsinAudioModelList(q, 1000); setCatalogSearchResults(list); } catch (e) { const msg = e instanceof Error ? e.message : "搜索失败"; setCatalogSearchError(msg); setCatalogSearchResults([]); } finally { setCatalogSearchLoading(false); } }, []); const loadCatalogModels = useCallback(async (brandName: string) => { setSelectedCatalogModelFromSearch(""); setCatalogModelsLoading(true); setCatalogModelsError(null); try { const list = await fetchLuxsinAudioModels(brandName); const sorted = [...list].sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" })); setCatalogModels(sorted); } catch (e) { const msg = e instanceof Error ? e.message : "加载失败"; setCatalogModelsError(msg); toast.error(eqUi.toastModelsFail); } finally { setCatalogModelsLoading(false); } }, [eqUi]); /* ── Effects ── */ // Auto-load brands when drawer opens useEffect(() => { if (!open) return; void loadCatalogBrands(); }, [open, loadCatalogBrands]); // Search debounce useEffect(() => { if (!open || brandDrawerTab !== "brands") return; const timer = window.setTimeout(() => { void searchCatalogByKeyword(brandSearchQuery); }, 260); return () => window.clearTimeout(timer); }, [open, brandDrawerTab, brandSearchQuery, searchCatalogByKeyword]); // Escape key useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [open, onClose]); // Body overflow lock useEffect(() => { if (!open) return; const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, [open]); if (!open) return null; return (
); })}
{brandDrawerTab === "brands" && (
setBrandSearchQuery(e.target.value)} placeholder={eqUi.searchPlaceholder} className="min-w-0 flex-1 bg-transparent text-[15px] text-black/80 outline-none placeholder:text-black/35" />
{brandSearchQuery.trim() && catalogSearchLoading && (
{eqUi.searching}
)} {!!brandSearchQuery.trim() && !catalogSearchLoading && catalogSearchError && (
{catalogSearchError}
)} {!!brandSearchQuery.trim() && !catalogSearchLoading && !catalogSearchError && catalogSearchResults.length === 0 && (
{eqUi.noSearchResults}
)} {!!brandSearchQuery.trim() && !catalogSearchLoading && !catalogSearchError && catalogSearchResults.map((item, idx) => ( ))} {!brandSearchQuery.trim() && catalogBrandsLoading && (
{eqUi.loading}
)} {!brandSearchQuery.trim() && !catalogBrandsLoading && catalogBrandsError && (
{catalogBrandsError}
)} {!brandSearchQuery.trim() && !catalogBrandsLoading && !catalogBrandsError && filteredCatalogBrands.length === 0 && (
{eqUi.noBrandMatch}
)} {!brandSearchQuery.trim() && !catalogBrandsLoading && !catalogBrandsError && filteredCatalogBrands.map((b) => ( ))}
)} {brandDrawerTab === "models" && (
{eqUi.brandLabel}{selectedCatalogBrand || "—"} {selectedCatalogModelFromSearch && ( {eqUi.modelFromSearchLabel} {selectedCatalogModelFromSearch} )}
{!selectedCatalogBrand && (
{eqUi.selectBrandFirst}
)} {!!selectedCatalogBrand && catalogModelsLoading && (
{eqUi.loading}
)} {!!selectedCatalogBrand && !catalogModelsLoading && catalogModelsError && (
{catalogModelsError}
)} {!!selectedCatalogBrand && !catalogModelsLoading && !catalogModelsError && catalogModels.length === 0 && (
{eqUi.noModelsForBrand}
)} {!!selectedCatalogBrand && !catalogModelsLoading && !catalogModelsError && catalogModels.map((m) => ( ))}
)} {brandDrawerTab === "target" && (
{eqUi.targetBrand} {selectedCatalogBrand || "—"}
{eqUi.targetModel} {selectedCatalogModelName || "—"}
{eqUi.targetForm} {selectedCatalogModelForm || eqUi.formAll}
{!selectedCatalogModelName && (
{eqUi.selectModelFirst}
)} {!!selectedCatalogModelName && availableCatalogTargets.length === 0 && (
{eqUi.noTargets}
)} {!!selectedCatalogModelName && availableCatalogTargets.map((target) => { const active = selectedCatalogTarget === target.name; return ( ); })}
)}
); }