PageSourceSearch

https://littlebirdelectronics.com.au/assets/controllers/ecommerce/quote_drawer_controller-5ac90a18.js

js littlebirdelectronics.com.au collected 2026-09-25 14:11:11 UTC 4,905 bytes, 149 lines download raw bytes

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 || ""} &times; ${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.