1import { Controller } from "@hotwired/stimulus" 2 3export default class extends Controller { 4 static targets = [ 5 "drawer", "overlay", "form", "submitButton", "submitText", 6 "spinner", "successMessage", "errorMessage", "errorText", "cartSummary" 7 ] 8 9 static values = { 10 url: String, 11 cartItems: Array 12 } 13 14 connect() { 15 this.loadSavedData() 16 this.renderCartSummary() 17 } 18 19 disconnect() { 20 document.body.style.overflow = "" 21 } 22 23 open() { 24 this.overlayTarget.classList.remove("hidden") 25 this.drawerTarget.classList.remove("translate-x-full") 26 document.body.style.overflow = "hidden" 27 } 28 29 close() { 30 this.drawerTarget.classList.add("translate-x-full") 31 this.overlayTarget.classList.add("hidden") 32 document.body.style.overflow = "" 33 } 34 35 async submit(event) { 36 event.preventDefault() 37 38 this.saveFormData() 39 this.showLoading() 40 41 const formData = new FormData(this.formTarget) 42 43 // Inherit the shipping method the user picked on the cart page so the 44 // quote uses the real rate (e.g., GoPeople same-day) instead of the 45 // server-side default. The radio lives outside this controller. 46 const selectedRate = document.querySelector('input[name="shipping_estimate"]:checked') 47 if (selectedRate) formData.set("shipping_method", selectedRate.value) 48 49 const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content 50 51 try { 52 const response = await fetch(this.urlValue, { 53 method: "POST", 54 headers: { 55 "X-CSRF-Token": csrfToken, 56 "Accept": "application/json" 57 }, 58 body: formData 59 }) 60 61 const data = await response.json() 62 63 if (data.success) { 64 this.showSuccess() 65 if (data.redirect_url) { 66 setTimeout(() => { window.location.href = data.redirect_url }, 2000) 67 } 68 } else { 69 this.showError(data.errors?.join(", ") || "Something went wrong. Please try again.") 70 } 71 } catch (error) { 72 this.showError("Network error. Please check your connection and try again.") 73 } 74 } 75 76 renderCartSummary() { 77 if (!this.hasCartSummaryTarget) return 78 79 const items = this.cartItemsValue 80 if (!items || items.length === 0) return 81 82 this.cartSummaryTarget.innerHTML = items.map(item => ` 83 <div class="flex items-center gap-3"> 84 <div class="h-10 w-10 flex-shrink-0 rounded-md bg-gray-100 dark:bg-gray-800 flex items-center justify-center overflow-hidden"> 85 ${item.image 86 ? `<img src="${item.image}" alt="" class="h-full w-full object-cover">` 87 : `<svg class="h-4 w-4 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0 0 22.5 18.75V5.25A2.25 2.25 0 0 0 20.25 3H3.75A2.25 2.25 0 0 0 1.5 5.25v13.5A2.25 2.25 0 0 0 3.75 21Z" /></svg>` 88 } 89 </div> 90 <div class="flex-1 min-w-0"> 91 <p class="text-sm text-gray-900 dark:text-white truncate">${this.escapeHtml(item.title)}</p> 92 <p class="text-xs text-gray-500 dark:text-gray-400">${item.sku || ""} × ${item.qty}</p> 93 </div> 94 <span class="text-sm font-medium text-gray-900 dark:text-white">$${(item.price * item.qty).toFixed(2)}</span> 95 </div> 96 `).join("") 97 } 98 99 showLoading() { 100 this.submitButtonTarget.disabled = true 101 this.submitTextTarget.textContent = "Submitting..." 102 this.spinnerTarget.classList.remove("hidden") 103 this.errorMessageTarget.classList.add("hidden") 104 } 105 106 showSuccess() { 107 this.formTarget.classList.add("hidden") 108 this.successMessageTarget.classList.remove("hidden") 109 this.spinnerTarget.classList.add("hidden") 110 localStorage.removeItem("quoteFormData") 111 } 112 113 showError(message) { 114 this.submitButtonTarget.disabled = false 115 this.submitTextTarget.textContent = "Submit Quote Request" 116 this.spinnerTarget.classList.add("hidden") 117 this.errorTextTarget.textContent = message 118 this.errorMessageTarget.classList.remove("hidden") 119 } 120 121 saveFormData() { 122 const formData = new FormData(this.formTarget) 123 const data = {} 124 formData.forEach((value, key) => { data[key] = value }) 125 localStorage.setItem("quoteFormData", JSON.stringify(data)) 126 } 127 128 loadSavedData() { 129 try { 130 const saved = JSON.parse(localStorage.getItem("quoteFormData")) 131 if (!saved) return 132
133 Object.entries(saved).forEach(([key, value]) => { 134 const input = this.formTarget.querySelector(`[name="${key}"]`) 135 if (input && !input.readOnly) { 136 input.value = value 137 } 138 }) 139 } catch { 140 // ignore parse errors 141 } 142 } 143 144 escapeHtml(text) { 145 const div = document.createElement("div") 146 div.textContent = text 147 return div.innerHTML 148 } 149}
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.