1import { Controller } from "@hotwired/stimulus" 2 3// Drives the single configurable buy card on /pricing. Two toggles reconfigure the 4// whole card between three states: 5// 6// fresh OFF, verified OFF -> Full Database CSV, $149 raw (default) 7// fresh OFF, verified ON -> Full Database CSV, $248 one-time 8// fresh ON -> StoreRadar Annual, $399/yr (verified forced "Included") 9// 10// All copy lives in the ERB (so prices/counts stay server-rendered and em-dash-free); 11// this controller only shows/hides the per-state groups and computes the live total. 12// Prices and endpoints come from data values, never hardcoded here. 13// 14// data-controller="buy-configurator" 15// data-buy-configurator-csv-price-value / addon-price-value / annual-price-value 16// data-buy-configurator-csv-url-value / annual-url-value 17// toggles: target="fresh" / "verified" (hidden checkboxes, change->buy-configurator#update) 18// groups: data-when="csv|annual|verified|raw|verified-any" (shown per state) 19// verified = CSV + ticked (the price breakdown); verified-any = verification 20// included at all, so it covers Annual too. 21// dynamic: target="price" / "button" / "fill" 22export default class extends Controller { 23 static targets = ["fresh", "verified", "price", "button", "fill"] 24 static values = { 25 csvPrice: Number, 26 addonPrice: Number, 27 annualPrice: Number, 28 csvUrl: String, 29 annualUrl: String 30 } 31 32 connect() { 33 this.onHashChange = this.onHashChange.bind(this) 34 window.addEventListener("hashchange", this.onHashChange) 35 this.applyHash() // honor a #buy / #buy-annual deep link on load 36 this.update() 37 this.scrollToDeepLink() // cross-page /pricing#buy: scroll the card into view on arrival 38 } 39 40 disconnect() { 41 window.removeEventListener("hashchange", this.onHashChange) 42 } 43 44 // A buy-intent CTA that scrolls here can request a state: #buy-annual opens the card 45 // in the StoreRadar Annual state, #buy opens it as the one-time CSV. Fires on load and 46 // on same-page hash changes (an in-page anchor click doesn't reconnect the controller). 47 onHashChange() { 48 this.applyHash() 49 this.update() 50 } 51 52 applyHash() { 53 if (!this.hasFreshTarget) return 54 const hash = window.location.hash 55 if (hash === "#buy-annual") this.freshTarget.checked = true 56 else if (hash === "#buy") this.freshTarget.checked = false 57 } 58 59 // A cross-page buy-intent CTA (/pricing#buy[-annual]), a reload, or a bookmark lands here 60 // with the hash already set. Native fragment scroll into the overflow-auto #main-content is 61 // unreliable (esp. after a Turbo visit), so we scroll the card in ourselves; rAF lets layout 62 // settle first. Same-page clicks don't reconnect the card, so this only runs on real arrivals. 63 scrollToDeepLink() { 64 const hash = window.location.hash 65 if (hash !== "#buy" && hash !== "#buy-annual") return 66 const target = document.getElementById(hash.slice(1)) 67 if (target) requestAnimationFrame(() => target.scrollIntoView({ behavior: "smooth", block: "start" })) 68 } 69 70 update() { 71 const annual = this.hasFreshTarget && this.freshTarget.checked 72 // Annual always includes verification; lock the verified toggle on while it's active. 73 if (this.hasVerifiedTarget) { 74 if (annual) { 75 this.verifiedTarget.checked = true 76 this.verifiedTarget.disabled = true 77 } else { 78 this.verifiedTarget.disabled = false 79 } 80 } 81 const verified = annual || (this.hasVerifiedTarget && this.verifiedTarget.checked) 82 83 // Per-state content groups. Toggle the `hidden` class (not the attribute) so it wins 84 // over any display utility; toggled elements carry no competing display class. 85 this.show('[data-when="csv"]', !annual) 86 this.show('[data-when="annual"]', annual) 87 this.show('[data-when="verified"]', !annual && verified) 88 this.show('[data-when="verified-any"]', verified) 89 this.show('[data-when="raw"]', !annual && !verified) 90 91 // Price + live total. 92 const total = annual ? this.annualPriceValue : this.csvPriceValue + (verified ? this.addonPriceValue : 0) 93 if (this.hasPriceTarget) this.priceTarget.textContent = `$${total}` 94 95 // Freshness bar: a sliver of orange for the 60-day window, full orange for the year. 96 if (this.hasFillTarget) { 97 this.fillTarget.style.width = annual ? "100%" : "16%" 98 this.fillTarget.classList.toggle("bg-orange-600", annual) 99 this.fillTarget.classList.toggle("bg-orange-300", !annual) 100 } 101 102 // CTA label + destination. 103 if (this.hasButtonTarget) { 104 if (annual) { 105 this.buttonTarget.textContent = `Go Annual for $${this.annualPriceValue}/yr` 106 this.buttonTarget.href = this.annualUrlValue 107 } else { 108 this.buttonTarget.textContent = `Buy the CSV for $${total}` 109 try { 110 const url = new URL(this.csvUrlValue, window.location.origin) 111 url.searchParams.set("verified", verified ? "true" : "false") 112 this.buttonTarget.href = url.pathname + url.search 113 } catch (_e) { 114 this.buttonTarget.href = this.csvUrlValue 115 } 116 } 117 } 118 } 119 120 show(selector, on) { 121 this.element.querySelectorAll(selector).forEach((el) => el.classList.toggle("hidden", !on)) 122 } 123}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.