stock check, variants in cart, variant buttons in product page

This commit is contained in:
elvira
2026-09-17 21:52:39 +03:00
parent fe55cb5f33
commit afa1993c53
27 changed files with 564 additions and 155 deletions
@@ -2,8 +2,18 @@ import { Controller } from '@hotwired/stimulus'
import { formatPrice } from '../utils/format-price'
export default class extends Controller {
static targets = ['price', 'image', 'swatch', 'colorName']
static values = { variants: Array, selected: Number }
static targets = ['price', 'image', 'swatch', 'colorName', 'stockError']
static values = {
variants: Array,
selected: Number,
stockCheckUrl: String,
// Two pre-rendered translated templates (see product/show.blade.php)
// rather than one — this controller doesn't reimplement Laravel's
// pluralization rules, it just picks whichever of these two the
// count actually needs and fills in the number.
stockErrorOne: String,
stockErrorMany: String,
}
connect() {
const params = new URLSearchParams(window.location.search)
@@ -13,6 +23,88 @@ export default class extends Controller {
this.selectedValue = urlId && this.variantsValue.find(v => v.id === urlId)
? urlId
: defaultId
// Capture phase, on this controller's own root element (an ancestor
// of the checkout module's add-to-cart <form>) — runs BEFORE that
// form's own bubble-phase submit handler (bbk-add-to-cart#add), so a
// failed check can stop it from ever reaching the module at all. The
// module itself is never touched or modified for this: it keeps
// validating server-side regardless, this is purely an up-front,
// storefront-owned check (see [[project_checkout_module]] for why
// that split matters — stock UX is a catalog concern, not something
// the portable checkout module should own) — and a REAL, live check
// against the backend (ProductController::checkStock(), reading the
// Eloquent model directly), not page-load data that can go stale.
this.onSubmitCapture = this.checkStock.bind(this)
this.element.addEventListener('submit', this.onSubmitCapture, true)
}
disconnect() {
this.element.removeEventListener('submit', this.onSubmitCapture, true)
}
checkStock(event) {
const form = event.target
if (!form.matches('.bbk-add-to-cart')) return
// The re-submit this itself triggers below, once the backend has
// confirmed the quantity is fine — let that one through to the
// module's own submit handler instead of checking a second time.
if (form.dataset.bbkStockChecked) {
delete form.dataset.bbkStockChecked
return
}
event.preventDefault()
event.stopPropagation()
this.verifyStock(form)
}
async verifyStock(form) {
this.clearStockError()
const submit = form.querySelector('[type="submit"]')
if (submit) submit.disabled = true
const purchasableId = form.querySelector('[data-bbk-purchasable-input]')?.value
const quantity = form.querySelector('[name="quantity"]')?.value || '1'
try {
const url = new URL(this.stockCheckUrlValue, window.location.origin)
url.searchParams.set('variant', purchasableId)
url.searchParams.set('quantity', quantity)
const response = await fetch(url, { headers: { Accept: 'application/json' } })
const data = await response.json()
if (!data.ok) {
this.showStockError(data.stock)
return
}
} catch {
// Network hiccup — fall through and let the checkout module's
// own server-side check have the final word rather than
// silently blocking the shopper here.
} finally {
if (submit) submit.disabled = false
}
form.dataset.bbkStockChecked = 'true'
form.requestSubmit()
}
showStockError(available) {
if (!this.hasStockErrorTarget) return
this.stockErrorTarget.textContent = available === 1
? this.stockErrorOneValue
: this.stockErrorManyValue.replace(':count', String(available))
this.stockErrorTarget.hidden = false
}
clearStockError() {
if (!this.hasStockErrorTarget) return
this.stockErrorTarget.hidden = true
}
selectVariant(event) {
@@ -30,6 +122,8 @@ export default class extends Controller {
const variant = this.variantsValue.find(v => v.id === id)
if (!variant) return
this.clearStockError()
if (this.hasPriceTarget && variant.price !== null) {
this.priceTarget.textContent = formatPrice(variant.price)
}