diff --git a/blocks/perfect-fit/perfect-fit.css b/blocks/perfect-fit/perfect-fit.css index 82aaeb4..ce913c3 100644 --- a/blocks/perfect-fit/perfect-fit.css +++ b/blocks/perfect-fit/perfect-fit.css @@ -216,3 +216,77 @@ body.perfect-fit-modal-open { grid-column: 1 / -1; } } + +/* search results */ +.perfect-fit-results:empty { + display: none; +} + +.perfect-fit-result-count { + margin: 24px 0 12px; + font-family: var(--heading-font-family); + font-size: 12px; + font-weight: 700; + letter-spacing: 1px; + text-transform: uppercase; + color: var(--conti-darkest-grey); +} + +.perfect-fit-results-list { + display: grid; + grid-template-columns: 1fr; + gap: 12px; +} + +@media (width >= 600px) { + .perfect-fit-results-list { + grid-template-columns: repeat(2, 1fr); + } +} + +.perfect-fit-result { + display: flex; + align-items: center; + gap: 14px; + padding: 10px; + border: 1px solid var(--conti-lightest-grey); + border-radius: 8px; + color: inherit; + text-decoration: none; + transition: border-color 0.2s ease, background-color 0.2s ease; +} + +.perfect-fit-result:hover, +.perfect-fit-result:focus-visible { + border-color: var(--conti-yellow); + background-color: var(--conti-lightest-grey); +} + +.perfect-fit-result-media { + flex: none; + width: 64px; + height: 64px; + display: flex; + align-items: center; + justify-content: center; +} + +.perfect-fit-result .perfect-fit-result-media img { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + +.perfect-fit-result-body h4 { + margin: 0; + font-family: var(--body-font-family); + font-size: 15px; + font-weight: 700; + line-height: 1.2; +} + +.perfect-fit-result-body p { + margin: 4px 0 0; + font-size: 12px; + color: var(--conti-darkest-grey); +} diff --git a/blocks/perfect-fit/perfect-fit.js b/blocks/perfect-fit/perfect-fit.js index a3def8e..22cebd7 100644 --- a/blocks/perfect-fit/perfect-fit.js +++ b/blocks/perfect-fit/perfect-fit.js @@ -1,121 +1,256 @@ +import { createOptimizedPicture } from '../../scripts/aem.js'; + /** - * "Find your perfect fit:" bar plus its tire-finder modal. The bar's three - * items open the modal on a matching tab (By Vehicle / By Tire Size / By - * Plate); each tab is a simple, non-functional sample search form. + * "Find your perfect fit:" bar plus its tire-finder modal. The three items + * open the modal on a matching tab (By Vehicle / By Tire Size / By Plate). + * Each tab searches the real product catalogue in /products.json and lists + * the matching tires. */ +const PRODUCTS_URL = '/products.json'; + const TABS = [ { id: 'vehicle', label: 'By Vehicle' }, { id: 'tire-size', label: 'By Tire Size' }, { id: 'plate', label: 'By Plate' }, ]; -function range(start, end, step = 1) { - const values = []; - for (let n = start; n <= end; n += step) values.push(n); - return values; -} +// A small curated vehicle set. Each model maps to a coarse class that lines +// up with the vehicleTypes recorded in products.json. +const VEHICLES = { + Chevrolet: { 'Silverado 1500': 'truck', Equinox: 'crossover', Malibu: 'car' }, + Ford: { 'F-150': 'truck', Explorer: 'suv', Escape: 'crossover' }, + Toyota: { RAV4: 'crossover', Camry: 'car', Tacoma: 'truck' }, + BMW: { X5: 'suv', '3 Series': 'car', X3: 'crossover' }, + Honda: { 'CR-V': 'crossover', Civic: 'car', Pilot: 'suv' }, + Tesla: { 'Model 3': 'car', 'Model Y': 'crossover' }, +}; +const STATES = ['California', 'Florida', 'Illinois', 'New York', 'Ohio', 'Texas']; +function range(start, end) { + const out = []; + for (let n = start; n <= end; n += 1) out.push(n); + return out; +} const YEARS = range(2015, 2026).reverse(); -const MAKES = ['Chevrolet', 'Ford', 'Toyota', 'BMW', 'Honda']; -const WIDTHS = range(185, 315, 10); -const ASPECT_RATIOS = range(40, 75, 5); -const RIM_DIAMETERS = range(15, 22); -const STATES = ['California', 'Florida', 'Illinois', 'New York', 'Ohio', 'Texas']; -/** - * Builds a labelled `` field. - * @returns {{ wrapper: Element, field: Element }} - */ -function createField(tag, id, labelText) { +// --- pure data helpers (exported for tests) --- + +/** Parses "225/45ZR17" into its width, aspect and rim, or null. */ +export function parseSize(str) { + const m = String(str).toUpperCase().match(/^(\d{3})\/(\d{2})Z?R(\d{2})$/); + return m ? { width: m[1], aspect: m[2], rim: m[3] } : null; +} + +/** Builds cascading width / aspect / rim option lists from every size. */ +export function sizeOptions(products) { + const widths = new Set(); + const aspectsByWidth = {}; + const rimsByWidthAspect = {}; + products.forEach((product) => (product.sizes || []).forEach((size) => { + const parsed = parseSize(size); + if (!parsed) return; + widths.add(parsed.width); + if (!aspectsByWidth[parsed.width]) aspectsByWidth[parsed.width] = new Set(); + aspectsByWidth[parsed.width].add(parsed.aspect); + const key = `${parsed.width}/${parsed.aspect}`; + if (!rimsByWidthAspect[key]) rimsByWidthAspect[key] = new Set(); + rimsByWidthAspect[key].add(parsed.rim); + })); + const num = (a, b) => Number(a) - Number(b); + const sortEntries = (obj) => Object.fromEntries( + Object.entries(obj).map(([k, v]) => [k, [...v].sort(num)]), + ); + return { + widths: [...widths].sort(num), + aspectsByWidth: sortEntries(aspectsByWidth), + rimsByWidthAspect: sortEntries(rimsByWidthAspect), + }; +} + +/** All products available in the exact width / aspect / rim size. */ +export function findBySize(products, { width, aspect, rim }) { + return products.filter((product) => (product.sizes || []).some((size) => { + const parsed = parseSize(size); + return parsed && parsed.width === width && parsed.aspect === aspect && parsed.rim === rim; + })); +} + +/** All products whose vehicleTypes match a coarse vehicle class keyword. */ +export function findByVehicleClass(products, vehicleClass) { + if (!vehicleClass) return []; + return products.filter((product) => (product.vehicleTypes || []).some( + (type) => type.toLowerCase().includes(vehicleClass), + )); +} + +// --- DOM helpers --- + +function createField(tag, name, labelText) { const wrapper = document.createElement('div'); wrapper.className = 'perfect-fit-field'; + const id = `perfect-fit-${name}`; const label = document.createElement('label'); label.setAttribute('for', id); label.textContent = labelText; const field = document.createElement(tag); field.id = id; - field.name = id; + field.name = name; wrapper.append(label, field); return { wrapper, field }; } -/** Fills a `