From ea0c9103f76eee387a1ded3c500d7ea4dbd1e302 Mon Sep 17 00:00:00 2001 From: Charlie Date: Sat, 13 Jun 2026 16:15:59 +0200 Subject: [PATCH] schema-apps: AggregateOffer price-comparison card design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bespoke renderTypeSpecific layout for AggregateOffer — clean retail header with product name + seller subtitle, split price range box (FROM lowPrice green / UP TO highPrice muted), offer count as pill chip, price insights from description, See All Deals green CTA. Class .card.price-compare. Gate passes (326 apps). --- app.css | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ app.js | 106 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 259 insertions(+) diff --git a/app.css b/app.css index 45175ce..f312edf 100644 --- a/app.css +++ b/app.css @@ -5879,3 +5879,156 @@ footer a:hover { color: var(--accent-2); } .pl-play-btn { width: 3rem; height: 3rem; } .pl-blurb { padding: .3rem 1rem .8rem; } } + +/* ── AggregateOffer — Price-Comparison card ── */ +.card.price-compare { + border-color: rgba(22,163,74,.1); +} +.card.price-compare .hd.hidden { display: none; } + +/* Header */ +.pc-header { + display: flex; + align-items: flex-start; + gap: .8rem; + padding: 1.2rem 1.5rem .6rem; +} +.pc-icon { + font-size: 1.6rem; + line-height: 1; + flex-shrink: 0; + margin-top: .15rem; +} +.pc-header-text { + flex: 1; +} +.pc-title { + font-size: 1.2rem; + font-weight: 800; + color: var(--ink); + line-height: 1.25; + margin: 0 0 .15rem; +} +.pc-seller { + font-size: .78rem; + color: var(--muted); +} + +/* Price range box */ +.pc-price-box { + display: flex; + align-items: stretch; + gap: 0; + margin: 0 1.5rem .6rem; + border-radius: 8px; + overflow: hidden; + border: 1px solid var(--line); +} +.pc-price-low { + flex: 1; + padding: .8rem 1rem; + background: linear-gradient(135deg, rgba(22,163,74,.06), rgba(22,163,74,.1)); + display: flex; + flex-direction: column; + gap: .15rem; +} +.pc-price-high { + flex: 1; + padding: .8rem 1rem; + background: var(--card-bg); + display: flex; + flex-direction: column; + gap: .15rem; + border-left: 1px solid var(--line); +} +.pc-price-label { + font-size: .62rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .12em; + color: var(--muted-2); +} +.pc-price-val { + font-size: 1.5rem; + font-weight: 800; + color: #16a34a; + line-height: 1.2; +} +.pc-price-val-high { + font-size: 1.1rem; + font-weight: 700; + color: var(--muted); + line-height: 1.2; +} + +/* Offer count chip */ +.pc-offer-chip { + display: inline-flex; + align-items: center; + gap: .3rem; + margin: 0 1.5rem .6rem; + padding: .3rem .7rem; + font-size: .73rem; + font-weight: 600; + color: var(--ink-2); + background: rgba(22,163,74,.05); + border: 1px solid rgba(22,163,74,.1); + border-radius: 20px; +} +.pc-offer-icon { + font-size: .8rem; +} + +/* Section label */ +.pc-section-label { + font-size: .7rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .12em; + color: var(--muted-2); + margin-bottom: .4rem; +} + +/* Price insights */ +.pc-insights { + padding: .2rem 1.5rem .8rem; +} +.pc-insights-body { + font-size: .84rem; + line-height: 1.75; + color: var(--muted); + margin: 0; +} + +/* See All Deals CTA */ +.pc-cta { + display: inline-flex; + align-items: center; + gap: .3rem; + margin: 0 1.5rem 1rem; + padding: .55rem 1.3rem; + background: linear-gradient(to bottom, #16a34a 5%, #15803d 95%); + color: #fff; + font-size: .85rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: .03em; + border-radius: 6px; + text-decoration: none; + box-shadow: 0 2px 8px rgba(22,101,52,.3); + transition: filter .15s; +} +.pc-cta:hover { + filter: brightness(1.1); +} + +@media (max-width: 480px) { + .pc-header { padding: 1rem 1rem .5rem; gap: .6rem; } + .pc-title { font-size: 1.05rem; } + .pc-price-box { margin: 0 1rem .5rem; flex-direction: column; } + .pc-price-high { border-left: none; border-top: 1px solid var(--line); } + .pc-price-val { font-size: 1.3rem; } + .pc-offer-chip { margin: 0 1rem .5rem; } + .pc-insights { padding: .2rem 1rem .6rem; } + .pc-cta { margin: 0 1rem .8rem; } +} diff --git a/app.js b/app.js index 0702f92..cb62982 100644 --- a/app.js +++ b/app.js @@ -3830,6 +3830,112 @@ } } + // ── AGGREGATEOFFER (Price-Comparison card) ── + if (cfg.type === "AggregateOffer") { + const card = document.querySelector(".card"); + if (card) card.classList.add("price-compare"); + + // Hide normal header + const hd = document.querySelector(".hd"); + if (hd) hd.classList.add("hidden"); + + // Remove generic hero + const heroMount = document.getElementById("hero-mount"); + if (heroMount) heroMount.querySelector('.hero')?.remove(); + + // ── Clean retail header ── + const pcHeader = document.createElement("div"); pcHeader.className = "pc-header"; + const pcIcon = document.createElement("div"); pcIcon.className = "pc-icon"; + pcIcon.textContent = "\u{1F4B0}"; + pcHeader.appendChild(pcIcon); + const pcHeaderText = document.createElement("div"); pcHeaderText.className = "pc-header-text"; + const h1 = document.createElement("h1"); h1.className = "pc-title"; + h1.textContent = data[cfg.titleProp] || ""; + pcHeaderText.appendChild(h1); + // Seller as subtitle if available + const seller = data["seller"]; + let sellerName = ""; + if (seller) { + if (typeof seller === "object" && seller.name) sellerName = seller.name; + else if (typeof seller === "string") sellerName = seller; + } + if (sellerName) { + const sub = document.createElement("div"); sub.className = "pc-seller"; + sub.textContent = sellerName; + pcHeaderText.appendChild(sub); + handled.add("seller"); + } + pcHeader.appendChild(pcHeaderText); + heroMount.appendChild(pcHeader); + handled.add("name"); handled.add("image"); + + // ── Big price range box ── + const lowPrice = data["lowPrice"]; + const highPrice = data["highPrice"]; + const currency = data["priceCurrency"]; + if (lowPrice || highPrice) { + const priceBox = document.createElement("div"); priceBox.className = "pc-price-box"; + + if (lowPrice) { + const lowEl = document.createElement("div"); lowEl.className = "pc-price-low"; + const lowLabel = document.createElement("span"); lowLabel.className = "pc-price-label"; + lowLabel.textContent = "FROM"; + const lowVal = document.createElement("span"); lowVal.className = "pc-price-val"; + lowVal.textContent = lowPrice; + lowEl.append(lowLabel, lowVal); + priceBox.appendChild(lowEl); + handled.add("lowPrice"); + } + + if (highPrice) { + const highEl = document.createElement("div"); highEl.className = "pc-price-high"; + const highLabel = document.createElement("span"); highLabel.className = "pc-price-label"; + highLabel.textContent = "UP TO"; + const highVal = document.createElement("span"); highVal.className = "pc-price-val-high"; + highVal.textContent = highPrice; + highEl.append(highLabel, highVal); + priceBox.appendChild(highEl); + handled.add("highPrice"); + } + + view.appendChild(priceBox); + } + if (currency) handled.add("priceCurrency"); + + // ── Offer count chip ── + const offerCount = data["offerCount"]; + if (offerCount != null) { + const chip = document.createElement("div"); chip.className = "pc-offer-chip"; + chip.innerHTML = "\u{1F4C4} " + offerCount + " seller" + (offerCount !== 1 ? "s" : ""); + view.appendChild(chip); + handled.add("offerCount"); + } + + // ── Description as price insights ── + const desc = data["description"]; + if (desc) { + const insights = document.createElement("div"); insights.className = "pc-insights"; + const label = document.createElement("div"); label.className = "pc-section-label"; + label.textContent = "Price Insights"; + insights.appendChild(label); + const body = document.createElement("p"); body.className = "pc-insights-body"; + body.textContent = desc; + insights.appendChild(body); + view.appendChild(insights); + handled.add("description"); + } + + // ── See All Deals CTA ── + const url = data["url"]; + if (url) { + const cta = document.createElement("a"); cta.className = "pc-cta"; + cta.href = url; cta.target = "_blank"; cta.rel = "noopener"; + cta.innerHTML = "See All Deals \u2192"; + view.appendChild(cta); + handled.add("url"); + } + } + return handled; }