/* =========================================================== Category + Product Detail + Search =========================================================== */ /* ---------------- CATEGORY ---------------- */ function CategoryPage({ slug }) { const cat = window.DATA.categories.find(c => c.slug === slug) || window.DATA.categories[0]; const [loading, setLoading] = useState(true); const [page, setPage] = useState(0); const [facetOpen, setFacetOpen] = useState(false); const [filters, setFilters] = useState({ reman: false, neww: false, online: false }); useEffect(() => { setLoading(true); const t = setTimeout(() => setLoading(false), 650); return () => clearTimeout(t); }, [slug]); let prods = window.DATA.productsFor(cat.slug); if (filters.reman) prods = prods.filter(p => p.is_reman); if (filters.neww) prods = prods.filter(p => !p.is_reman); if (filters.online) prods = prods.filter(p => p.is_online_orderable); const groups = {}; prods.forEach(p => { const gl = p.grouping_label || ""; (groups[gl] = groups[gl] || []).push(p); }); const Facets = React.createElement("aside", { className: "facets" + (facetOpen ? " open" : "") }, facetOpen ? React.createElement("div", { className: "between", style: { marginBottom: 16 } }, React.createElement("h3", { style: { fontFamily: "var(--display)" } }, "Filters"), React.createElement("button", { className: "icon-btn", onClick: () => setFacetOpen(false) }, React.createElement(Icon, { name: "close" }))) : null, React.createElement("div", { className: "facet-group" }, React.createElement("h4", null, "Type"), [["reman", "Remanufactured"], ["neww", "New / OEM"]].map(([k, l]) => React.createElement("label", { key: k, className: "facet" }, React.createElement("input", { type: "checkbox", checked: filters[k], onChange: e => setFilters(f => ({ ...f, [k]: e.target.checked })) }), l))), React.createElement("div", { className: "facet-group" }, React.createElement("h4", null, "Availability"), React.createElement("label", { className: "facet" }, React.createElement("input", { type: "checkbox", checked: filters.online, onChange: e => setFilters(f => ({ ...f, online: e.target.checked })) }), "Orderable online")), React.createElement("div", { className: "facet-group" }, React.createElement("h4", null, "Sub-category"), cat.children.map(ch => React.createElement("label", { key: ch.slug, className: "facet" }, React.createElement("input", { type: "checkbox" }), ch.name))) ); return React.createElement("div", null, React.createElement("div", { className: "cat-hero" }, React.createElement("div", { className: "wrap" }, React.createElement(Crumbs, { items: [{ label: "Home", to: "/" }, { label: "Catalog", to: "/c/" + cat.slug }, { label: cat.name }] }), React.createElement("h1", null, cat.name), React.createElement("p", null, cat.blurb, " All parts cross-referenced to OEM and competitor numbers."))), React.createElement("div", { className: "wrap" }, React.createElement("div", { className: "cat-layout" }, Facets, React.createElement("div", { className: "cat-main" }, React.createElement("div", { className: "cat-toolbar" }, React.createElement("button", { className: "btn btn-ghost btn-sm mobile-facet-btn", onClick: () => setFacetOpen(true) }, React.createElement(Icon, { name: "filter", size: 16 }), "Filters"), React.createElement("span", { className: "ct-count" }, loading ? "Loading…" : prods.length + " parts"), React.createElement("select", { "aria-label": "Sort" }, React.createElement("option", null, "Sort: Relevance"), React.createElement("option", null, "Price: Low to High"), React.createElement("option", null, "Reman first"))), loading ? React.createElement("div", { className: "pgrid" }, Array.from({ length: 8 }).map((_, i) => React.createElement(SkeletonCard, { key: i }))) : Object.keys(groups).map(g => React.createElement("div", { key: g || "_all", className: "grouping-block" }, g ? React.createElement("div", { className: "grouping-head" }, React.createElement("h3", null, g), React.createElement("span", { className: "gc" }, groups[g].length + " items")) : null, React.createElement("div", { className: "pgrid" }, groups[g].map(p => React.createElement(ProductCard, { key: p.id, product: p }))) )), !loading ? React.createElement("div", { className: "pagination" }, ["1", "2", "3", "›"].map((n, i) => React.createElement("button", { key: n, className: i === 0 ? "active" : "", onClick: () => setPage(i) }, n))) : null ) ) ) ); } /* ---------------- PDP ---------------- */ function ProductPage({ slug }) { const product = window.DATA.productBySlug(slug); const cart = useCart(); const toast = useToast(); const { navigate } = useRouter(); const quote = useQuote(); const [vi, setVi] = useState(0); const [gi, setGi] = useState(0); const [qty, setQty] = useState(1); if (!product) return React.createElement(NotFound, null); const v = product.variants[vi]; const orderable = product.is_online_orderable && v.is_orderable && v.price !== null; const cat = window.DATA.categories.find(c => c.slug === product.category); const attrLabels = { diameter: "Diameter", torque_rating: "Torque rating", spline: "Spline", plate_load: "Plate load", num_springs: "Dampers", facings: "Facing material", flywheel_bore: "Flywheel bore", weight_class: "Weight class" }; const attrs = Object.entries(v.attributes || {}).filter(([k, val]) => val && val !== "—"); const addCart = () => { cart.add({ variant_id: v.id, sku: v.sku, name: product.name, product_slug: product.slug, quantity: qty, unit_price: v.price, core_charge: v.core_charge, image: product.images[0] }); toast.push("Added to cart", { icon: "cart", action: { to: "/cart", label: "View cart" } }); }; const requestQuote = () => { quote.addItem({ variant_id: v.id, sku: v.sku, name: product.name, product_slug: product.slug, quantity: qty, price: v.price, core_charge: v.core_charge, image: product.images[0], attributes: v.attributes, part_numbers: v.part_numbers }); navigate("/quote"); }; const byKind = { OEM: [], interchange: [], competitor: [] }; (v.part_numbers || []).forEach(pn => { (byKind[pn.kind] = byKind[pn.kind] || []).push(pn); }); const kindLabel = { OEM: "OEM / original", interchange: "Interchange", competitor: "Competitor cross-ref" }; const interchangeNums = (v.part_numbers || []).filter(p => p.kind !== "OEM").map(p => p.number); return React.createElement("div", { className: "pdp" }, React.createElement("div", { className: "wrap" }, React.createElement(Crumbs, { items: [{ label: "Home", to: "/" }, { label: cat ? cat.name : "Catalog", to: "/c/" + product.category }, { label: product.name }] }), React.createElement("div", { className: "pdp-grid", style: { marginTop: 18 } }, // gallery React.createElement("div", null, React.createElement("div", { className: "gallery-main" }, React.createElement(Img, { src: (product.images && (product.images[gi] || product.images[0])) ? (product.images[gi] || product.images[0]).url : "", alt: product.name, ratio: "4/3", phLabel: product.name })), (product.images && product.images.length > 1) ? React.createElement("div", { className: "gallery-thumbs" }, product.images.slice(0, 4).map((im, i) => React.createElement("button", { key: i, className: "gt" + (i === gi ? " active" : ""), onClick: () => setGi(i), "aria-label": "View image " + (i + 1) }, React.createElement(Img, { src: im.url, alt: product.name, ratio: "1/1" })))) : null ), // info React.createElement("div", { className: "pdp-info" }, React.createElement("div", { className: "pdp-badges" }, product.is_reman ? React.createElement("span", { className: "badge badge-reman" }, "Remanufactured") : React.createElement("span", { className: "badge badge-oem" }, "New / OEM"), React.createElement("span", { className: "chip" }, product.weight_class), !orderable ? React.createElement("span", { className: "badge badge-call" }, "Quote only") : null), React.createElement("h1", null, product.name), React.createElement("div", { className: "pdp-sku mono" }, "D&W SKU · " + v.sku + " · MFR " + v.mfr_part_number), React.createElement("p", { className: "muted", style: { marginTop: 14, fontSize: 15.5 } }, product.description), // variant selector product.variants.length > 1 ? React.createElement("div", { className: "variant-select" }, React.createElement("div", { className: "vlabel" }, "Select configuration"), product.variants.map((vv, i) => React.createElement("button", { key: vv.id, className: "variant-opt" + (i === vi ? " sel" : ""), onClick: () => setVi(i) }, React.createElement("span", null, React.createElement("div", { className: "vo-name" }, vv.name), React.createElement("div", { className: "vo-sku mono" }, vv.sku)), React.createElement(Price, { variant: vv, size: "sm" })))) : null, React.createElement("div", { className: "pdp-price-row" }, React.createElement(Price, { variant: v, size: "lg" })), v.core_charge ? React.createElement("div", { className: "core-callout" }, React.createElement(Icon, { name: "spark", size: 18 }), React.createElement("span", null, React.createElement("b", null, money(v.core_charge) + " refundable core charge"), " — billed now and credited back when your old unit is returned to our core bank. Freight-prepaid both ways on qualifying orders.")) : null, // actions React.createElement("div", { className: "pdp-actions" }, orderable ? React.createElement(Stepper, { value: qty, onChange: setQty }) : null, orderable ? React.createElement("button", { className: "btn btn-primary btn-lg", style: { flex: 1, minWidth: 180 }, onClick: addCart }, React.createElement(Icon, { name: "cart", size: 18 }), "Add to cart") : React.createElement("button", { className: "btn btn-purple btn-lg", style: { flex: 1, minWidth: 180 }, onClick: requestQuote }, React.createElement(Icon, { name: "quote", size: 18 }), "Request a quote"), React.createElement(AddToQuoteBtn, { product, variant: v, size: "lg", label: "Add to quote" })), React.createElement("a", { href: "tel:4102358829", className: "navlink", style: { paddingLeft: 0 } }, React.createElement(Icon, { name: "phone", size: 16 }), "Questions? Call 410-235-8829") ) ), // spec + crossref React.createElement("div", { className: "pdp-section" }, React.createElement("div", { className: "pdp-grid", style: { alignItems: "start" } }, React.createElement("div", null, React.createElement("h2", null, "Specifications"), attrs.length ? React.createElement("dl", { style: { margin: 0 } }, attrs.map(([k, val]) => React.createElement("div", { key: k, className: "spec-row" }, React.createElement("dt", null, attrLabels[k] || k), React.createElement("dd", null, val))) ) : React.createElement("p", { className: "muted" }, "Specs available on request — call or add to a quote.") ), React.createElement("div", null, React.createElement("h2", null, "Cross-reference"), React.createElement("div", { className: "crossref-panel" }, ["OEM", "interchange", "competitor"].filter(k => (byKind[k] || []).length).map(k => React.createElement("div", { key: k, className: "crossref-kind" }, React.createElement("div", { className: "ck-head" }, kindLabel[k]), React.createElement("div", { className: "crossref-nums" }, byKind[k].map((pn, i) => React.createElement("span", { key: i, className: "xnum" + (k === "OEM" ? " oem" : "") }, pn.number, React.createElement("span", { className: "xsrc" }, pn.source))))))), interchangeNums.length ? React.createElement("p", { className: "seo-copy" }, "This part replaces ", React.createElement("b", null, interchangeNums.join(", ")), ". Searching one of those numbers? You're in the right place — the ", v.sku, " is a direct interchange.") : null ) ) ), // fitments v.fitments && v.fitments.length ? React.createElement("div", { className: "pdp-section" }, React.createElement("h2", null, "Fitment"), React.createElement("table", { className: "fit-table" }, React.createElement("thead", null, React.createElement("tr", null, ["Make", "Model", "Years", "Engine", "Position"].map(h => React.createElement("th", { key: h }, h)))), React.createElement("tbody", null, v.fitments.map((f, i) => React.createElement("tr", { key: i }, React.createElement("td", null, React.createElement("b", null, f.make)), React.createElement("td", null, f.model), React.createElement("td", null, f.year_from + "–" + f.year_to), React.createElement("td", null, f.engine), React.createElement("td", null, f.position))))) ) : null ), // mobile sticky bar React.createElement("div", { className: "sticky-bar" }, React.createElement("div", { className: "sb-price" }, React.createElement(Price, { variant: v, size: "sm" })), React.createElement(AddToQuoteBtn, { product, variant: v, size: "lg", label: "Quote" }), orderable ? React.createElement("button", { className: "btn btn-primary btn-lg", onClick: addCart }, "Add to cart") : React.createElement("a", { href: "tel:4102358829", className: "btn btn-purple btn-lg" }, React.createElement(Icon, { name: "phone", size: 16 }), "Call")) ); } /* ---------------- SEARCH ---------------- */ function SearchPage({ q }) { const [query, setQuery] = useState(q || ""); const [loading, setLoading] = useState(true); useEffect(() => { setQuery(q || ""); setLoading(true); const t = setTimeout(() => setLoading(false), 500); return () => clearTimeout(t); }, [q]); const { navigate } = useRouter(); const results = useMemo(() => window.DATA.search(query), [query]); const groups = { exact_pn: [], prefix_pn: [], fuzzy_pn: [], keyword: [] }; results.forEach(r => groups[r.match_type].push(r)); const groupMeta = { exact_pn: { title: "Exact part-number match", chip: "exact" }, prefix_pn: { title: "Part-number starts with", chip: "" }, fuzzy_pn: { title: "Part-number contains", chip: "" }, keyword: { title: "Keyword matches", chip: "" }, }; return React.createElement("div", { className: "search-page" }, React.createElement("div", { className: "wrap" }, React.createElement("div", { className: "search-head" }, React.createElement("span", { className: "eyebrow" }, "Search"), React.createElement("h1", null, query ? "Results for “" + query + "”" : "Search parts"), React.createElement("div", { className: "bigsearch", style: { maxWidth: 560, marginTop: 16 } }, React.createElement("input", { value: query, autoFocus: true, onChange: e => setQuery(e.target.value), placeholder: "Part #, OEM, or interchange…" }), React.createElement("button", { onClick: () => navigate("/search?q=" + encodeURIComponent(query)) }, React.createElement(Icon, { name: "search", size: 18 }))) ), loading ? React.createElement("div", { style: { display: "grid", gap: 12 } }, Array.from({ length: 4 }).map((_, i) => React.createElement("div", { key: i, className: "sk", style: { height: 92, borderRadius: 12 } }))) : results.length === 0 ? React.createElement("div", { style: { padding: "40px 0", textAlign: "center", color: "var(--ink-60)" } }, React.createElement("p", { style: { fontSize: 18, fontWeight: 600, color: "var(--ink)" } }, "No matches for “" + query + "”"), React.createElement("p", null, "Try fewer characters of the part number, or "), React.createElement(Link, { to: "/quote", className: "btn btn-purple", style: { marginTop: 12 } }, "Build a quote with a custom part")) : Object.keys(groups).filter(k => groups[k].length).map(k => React.createElement("div", { key: k, className: "match-group" }, React.createElement("div", { className: "match-group-head" }, React.createElement("h3", null, groupMeta[k].title), React.createElement("span", { className: "chip" }, groups[k].length)), React.createElement("div", { style: { display: "grid", gap: 10 } }, groups[k].map(r => React.createElement("div", { key: r.variant_id, className: "search-row", onClick: () => navigate("/p/" + r.product_slug) }, React.createElement("div", { className: "sr-ph ph", "data-label": "" }), React.createElement("div", { style: { flex: 1, minWidth: 0 } }, React.createElement("div", { className: "sr-name" }, r.name), React.createElement("div", { className: "sr-sku mono" }, r.sku), r.matched_number ? React.createElement("span", { className: "chip matched-chip" + (k === "exact_pn" ? " exact" : ""), style: { marginTop: 8 } }, React.createElement(Icon, { name: "check", size: 13 }), "matched on PN ", React.createElement("span", { className: "mono" }, r.matched_number)) : null), React.createElement("div", { style: { textAlign: "right", flex: "none" } }, r.price !== null ? React.createElement("div", { className: "price-now", style: { fontFamily: "var(--display)", fontSize: 18 } }, money(r.price)) : React.createElement("span", { className: "price-call", style: { fontSize: 14 } }, "Call"), r.is_reman ? React.createElement("span", { className: "badge badge-reman", style: { marginTop: 6 } }, "Reman") : null), React.createElement(Icon, { name: "chevron", size: 18, style: { color: "var(--ink-30)", flex: "none" } }) ))) )) ) ); } function NotFound() { return React.createElement("div", { className: "wrap", style: { padding: "80px 0", textAlign: "center" } }, React.createElement("h1", { style: { fontSize: 40 } }, "Part not found"), React.createElement("p", { className: "muted", style: { margin: "12px 0 24px" } }, "That page or part number isn't in the catalog yet."), React.createElement(Link, { to: "/", className: "btn btn-ink" }, "Back home")); } Object.assign(window, { CategoryPage, ProductPage, SearchPage, NotFound });