0 && !hasMultipleVariants) { expanded = true; pendingCollapse = false }"
@mouseleave="if (qty > 0 && !hasMultipleVariants) { if (loading) { pendingCollapse = true } else { expanded = false } }"
x-data="{
qty: 0,
hasQty: false,
maxQty: 50,
expanded: false,
loading: false,
pendingCollapse: false,
circleHover: false,
timer: null,
productId: 53,
quickAddCartId: '53',
hasMultipleVariants: false,
// Solo se usa con hasMultipleVariants -- mapea cartId real (id-variante)
// a qty, para poder sumar/restar por línea de carrito sin perder el
// conteo de las demás variantes ya agregadas cuando llega un evento de
// una sola de ellas. Ver el cast a objeto en el bloque @php de arriba.
variantQuantities: {},
// Evitan que el listener de window de más abajo vuelva a aplicar la propia
// mutación que esta misma instancia ya hizo localmente (Livewire.dispatch()
// llega también a esta instancia como evento de window, no solo a las demás).
suppressNextAdd: false,
suppressNextRemove: false,
// Suma todas las líneas de variantQuantities -- el número que ve el
// usuario en el círculo es cuántas unidades de este producto (en
// cualquier presentación) hay en el carrito, no de una variante puntual.
recalcQtyFromVariants() {
this.qty = Object.values(this.variantQuantities).reduce((sum, n) => sum + n, 0);
this.hasQty = this.qty > 0;
},
// window.dispatchEvent (NO Livewire.dispatch) a propósito -- el modal
// (variant-picker.blade.php) ya no es un componente Livewire que vuelve a
// consultar el producto al abrir; escucha este mismo evento de window y
// pinta con el payload que ya viaja acá, calculado una sola vez en el
// server al renderizar ESTA card (ver Product::quickViewPayload()). Así
// abre al instante, sin esperar ningún roundtrip -- Livewire.dispatch()
// sí implicaría uno (cualquier acción Livewire es una request).
//
// qtyInCart de cada variante SÍ se refresca acá antes de despachar --
// ese campo del payload es correcto recién al cargar la página, pero
// queda desactualizado si el cliente agrega/quita algo (de esta misma
// presentación o de otra) DESPUÉS, sin recargar (ej. reabre este mismo
// modal tras agregar una presentación una primera vez). variantQuantities
// de este círculo ya se mantiene sincronizado en vivo con el carrito real
// (ver los 3 listeners de window más abajo) -- se reusa esa MISMA fuente
// en vez de duplicarla, solo para pisar qtyInCart con el valor vigente
// justo antes de abrir.
openVariantPicker() {
const payload = null;
if (payload) {
payload.variants = payload.variants.map((variant) => ({
...variant,
qtyInCart: this.variantQuantities[this.productId + '-' + variant.name.replace(/ /g, '')] || 0,
}));
}
window.dispatchEvent(new CustomEvent('open-variant-picker', { detail: payload }));
},
addFirstTime() {
this.qty = 1;
this.hasQty = true;
this.expanded = true;
this.suppressNextAdd = true;
// dispatchId único por clic (no por instancia): deja que CartCounter
// descarte la segunda aplicación cuando sus 2 instancias montadas
// reciben este mismo evento -- ver nota en CartCounter::add().
const dispatchId = (window.crypto && crypto.randomUUID) ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
Livewire.dispatch('addToCart', { productId: this.quickAddCartId, qty: 1, dispatchId });
},
reopen() {
this.expanded = true;
},
changeQty(amount) {
// Tope real (ver maxQty arriba, Product::stockParaListado()): el botón
// de incrementar no debe pedir más de lo que hay -- respaldo del
// :disabled nativo (ver el markup más abajo), por si igual llega el
// click (estado de Alpine desfasado, DevTools). OJO al editar este
// comentario: todo este objeto x-data vive dentro de un atributo HTML
// entre comillas dobles, así que ninguna comilla doble literal puede
// aparecer acá adentro (ni en código ni en comentarios) -- cierra ese
// atributo antes de tiempo para el parser del navegador. BUG real ya
// encontrado y confirmado en vivo (screenshot: el resto del objeto
// x-data se pintaba como texto suelto encima de la card).
if (amount > 0 && this.qty >= this.maxQty) return;
const next = this.qty + amount;
clearTimeout(this.timer);
if (next < 1) {
this.timer = setTimeout(() => {
this.loading = true;
this.suppressNextRemove = true;
Livewire.dispatch('removeFromCart', { productId: this.quickAddCartId });
setTimeout(() => {
this.expanded = false; // inicia el leave del pill; loading sigue en true, el spinner tapa el número
setTimeout(() => {
this.hasQty = false; // circulo ya visible con el numero anterior; inicia su propio crossfade al icono mas
setTimeout(() => {
this.qty = 0;
this.loading = false;
this.pendingCollapse = false;
}, 150); // debe coincidir con la duración de x-transition.opacity.duration.150ms del span numérico
}, 250); // debe coincidir con la duración de product-card-add-pill-leave-active
}, 700);
}, 500);
return;
}
this.qty = next;
this.loading = true;
this.timer = setTimeout(() => {
Livewire.dispatch('updateCartQuantity', { productId: this.quickAddCartId, qty: this.qty });
setTimeout(() => {
this.loading = false;
if (this.pendingCollapse) {
this.expanded = false;
this.pendingCollapse = false;
}
}, 900);
}, 500);
}
}"
x-init="
$watch('expanded', (value) => {
if (value) { circleHover = false; }
$dispatch('cart-pill-toggle', { show: expanded || circleHover });
});
$watch('circleHover', () => $dispatch('cart-pill-toggle', { show: expanded || circleHover }));
// Mantiene sincronizado este botón cuando el carrito cambia desde otro
// lugar (drawer, VariantPicker, u otra instancia de esta misma card en
// otra sección de la página) -- Livewire.dispatch() llega a estos 3
// eventos como CustomEvent en window, con los mismos campos que ya usan
// CartCounter/CartDrawer (productId, qty).
//
// Con hasMultipleVariants, event.detail.productId es el cartId REAL
// con sufijo de variante (id-variante, ver VariantPicker::cartId() y
// VariantPicker::addToCart(), que dispara 'updateCartQuantity' al
// agregar desde el modal) -- nunca va a ser igual a quickAddCartId
// (que para estos productos es solo el id plano, sin variante, ver el
// bloque PHP de arriba). Por eso se compara por PREFIJO (id + guion)
// en vez de igualdad exacta, y se acumula por variante en
// variantQuantities en vez de pisar qty directo -- así una variante
// que cambia no borra el conteo de las demás ya agregadas.
window.addEventListener('addToCart', (event) => {
if (hasMultipleVariants) {
if (!event.detail.productId.startsWith(productId + '-')) return;
if (suppressNextAdd) { suppressNextAdd = false; return; }
variantQuantities[event.detail.productId] = (variantQuantities[event.detail.productId] || 0) + event.detail.qty;
recalcQtyFromVariants();
return;
}
if (event.detail.productId !== quickAddCartId) return;
if (suppressNextAdd) { suppressNextAdd = false; return; }
qty += event.detail.qty;
hasQty = qty > 0;
});
window.addEventListener('updateCartQuantity', (event) => {
if (hasMultipleVariants) {
if (!event.detail.productId.startsWith(productId + '-')) return;
variantQuantities[event.detail.productId] = event.detail.qty;
recalcQtyFromVariants();
return;
}
if (event.detail.productId !== quickAddCartId) return;
qty = event.detail.qty;
hasQty = qty > 0;
});
window.addEventListener('removeFromCart', (event) => {
if (hasMultipleVariants) {
if (!event.detail.productId.startsWith(productId + '-')) return;
delete variantQuantities[event.detail.productId];
recalcQtyFromVariants();
expanded = false;
loading = false;
pendingCollapse = false;
return;
}
if (event.detail.productId !== quickAddCartId) return;
if (suppressNextRemove) { suppressNextRemove = false; return; }
qty = 0;
hasQty = false;
expanded = false;
loading = false;
pendingCollapse = false;
});
">
Herrera
Cocoa Blend
Cocoa Blend
S/ 12.00
Edición Limitada
0 && !hasMultipleVariants) { expanded = true; pendingCollapse = false }"
@mouseleave="if (qty > 0 && !hasMultipleVariants) { if (loading) { pendingCollapse = true } else { expanded = false } }"
x-data="{
qty: 0,
hasQty: false,
maxQty: 50,
expanded: false,
loading: false,
pendingCollapse: false,
circleHover: false,
timer: null,
productId: 52,
quickAddCartId: '52',
hasMultipleVariants: false,
// Solo se usa con hasMultipleVariants -- mapea cartId real (id-variante)
// a qty, para poder sumar/restar por línea de carrito sin perder el
// conteo de las demás variantes ya agregadas cuando llega un evento de
// una sola de ellas. Ver el cast a objeto en el bloque @php de arriba.
variantQuantities: {},
// Evitan que el listener de window de más abajo vuelva a aplicar la propia
// mutación que esta misma instancia ya hizo localmente (Livewire.dispatch()
// llega también a esta instancia como evento de window, no solo a las demás).
suppressNextAdd: false,
suppressNextRemove: false,
// Suma todas las líneas de variantQuantities -- el número que ve el
// usuario en el círculo es cuántas unidades de este producto (en
// cualquier presentación) hay en el carrito, no de una variante puntual.
recalcQtyFromVariants() {
this.qty = Object.values(this.variantQuantities).reduce((sum, n) => sum + n, 0);
this.hasQty = this.qty > 0;
},
// window.dispatchEvent (NO Livewire.dispatch) a propósito -- el modal
// (variant-picker.blade.php) ya no es un componente Livewire que vuelve a
// consultar el producto al abrir; escucha este mismo evento de window y
// pinta con el payload que ya viaja acá, calculado una sola vez en el
// server al renderizar ESTA card (ver Product::quickViewPayload()). Así
// abre al instante, sin esperar ningún roundtrip -- Livewire.dispatch()
// sí implicaría uno (cualquier acción Livewire es una request).
//
// qtyInCart de cada variante SÍ se refresca acá antes de despachar --
// ese campo del payload es correcto recién al cargar la página, pero
// queda desactualizado si el cliente agrega/quita algo (de esta misma
// presentación o de otra) DESPUÉS, sin recargar (ej. reabre este mismo
// modal tras agregar una presentación una primera vez). variantQuantities
// de este círculo ya se mantiene sincronizado en vivo con el carrito real
// (ver los 3 listeners de window más abajo) -- se reusa esa MISMA fuente
// en vez de duplicarla, solo para pisar qtyInCart con el valor vigente
// justo antes de abrir.
openVariantPicker() {
const payload = null;
if (payload) {
payload.variants = payload.variants.map((variant) => ({
...variant,
qtyInCart: this.variantQuantities[this.productId + '-' + variant.name.replace(/ /g, '')] || 0,
}));
}
window.dispatchEvent(new CustomEvent('open-variant-picker', { detail: payload }));
},
addFirstTime() {
this.qty = 1;
this.hasQty = true;
this.expanded = true;
this.suppressNextAdd = true;
// dispatchId único por clic (no por instancia): deja que CartCounter
// descarte la segunda aplicación cuando sus 2 instancias montadas
// reciben este mismo evento -- ver nota en CartCounter::add().
const dispatchId = (window.crypto && crypto.randomUUID) ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
Livewire.dispatch('addToCart', { productId: this.quickAddCartId, qty: 1, dispatchId });
},
reopen() {
this.expanded = true;
},
changeQty(amount) {
// Tope real (ver maxQty arriba, Product::stockParaListado()): el botón
// de incrementar no debe pedir más de lo que hay -- respaldo del
// :disabled nativo (ver el markup más abajo), por si igual llega el
// click (estado de Alpine desfasado, DevTools). OJO al editar este
// comentario: todo este objeto x-data vive dentro de un atributo HTML
// entre comillas dobles, así que ninguna comilla doble literal puede
// aparecer acá adentro (ni en código ni en comentarios) -- cierra ese
// atributo antes de tiempo para el parser del navegador. BUG real ya
// encontrado y confirmado en vivo (screenshot: el resto del objeto
// x-data se pintaba como texto suelto encima de la card).
if (amount > 0 && this.qty >= this.maxQty) return;
const next = this.qty + amount;
clearTimeout(this.timer);
if (next < 1) {
this.timer = setTimeout(() => {
this.loading = true;
this.suppressNextRemove = true;
Livewire.dispatch('removeFromCart', { productId: this.quickAddCartId });
setTimeout(() => {
this.expanded = false; // inicia el leave del pill; loading sigue en true, el spinner tapa el número
setTimeout(() => {
this.hasQty = false; // circulo ya visible con el numero anterior; inicia su propio crossfade al icono mas
setTimeout(() => {
this.qty = 0;
this.loading = false;
this.pendingCollapse = false;
}, 150); // debe coincidir con la duración de x-transition.opacity.duration.150ms del span numérico
}, 250); // debe coincidir con la duración de product-card-add-pill-leave-active
}, 700);
}, 500);
return;
}
this.qty = next;
this.loading = true;
this.timer = setTimeout(() => {
Livewire.dispatch('updateCartQuantity', { productId: this.quickAddCartId, qty: this.qty });
setTimeout(() => {
this.loading = false;
if (this.pendingCollapse) {
this.expanded = false;
this.pendingCollapse = false;
}
}, 900);
}, 500);
}
}"
x-init="
$watch('expanded', (value) => {
if (value) { circleHover = false; }
$dispatch('cart-pill-toggle', { show: expanded || circleHover });
});
$watch('circleHover', () => $dispatch('cart-pill-toggle', { show: expanded || circleHover }));
// Mantiene sincronizado este botón cuando el carrito cambia desde otro
// lugar (drawer, VariantPicker, u otra instancia de esta misma card en
// otra sección de la página) -- Livewire.dispatch() llega a estos 3
// eventos como CustomEvent en window, con los mismos campos que ya usan
// CartCounter/CartDrawer (productId, qty).
//
// Con hasMultipleVariants, event.detail.productId es el cartId REAL
// con sufijo de variante (id-variante, ver VariantPicker::cartId() y
// VariantPicker::addToCart(), que dispara 'updateCartQuantity' al
// agregar desde el modal) -- nunca va a ser igual a quickAddCartId
// (que para estos productos es solo el id plano, sin variante, ver el
// bloque PHP de arriba). Por eso se compara por PREFIJO (id + guion)
// en vez de igualdad exacta, y se acumula por variante en
// variantQuantities en vez de pisar qty directo -- así una variante
// que cambia no borra el conteo de las demás ya agregadas.
window.addEventListener('addToCart', (event) => {
if (hasMultipleVariants) {
if (!event.detail.productId.startsWith(productId + '-')) return;
if (suppressNextAdd) { suppressNextAdd = false; return; }
variantQuantities[event.detail.productId] = (variantQuantities[event.detail.productId] || 0) + event.detail.qty;
recalcQtyFromVariants();
return;
}
if (event.detail.productId !== quickAddCartId) return;
if (suppressNextAdd) { suppressNextAdd = false; return; }
qty += event.detail.qty;
hasQty = qty > 0;
});
window.addEventListener('updateCartQuantity', (event) => {
if (hasMultipleVariants) {
if (!event.detail.productId.startsWith(productId + '-')) return;
variantQuantities[event.detail.productId] = event.detail.qty;
recalcQtyFromVariants();
return;
}
if (event.detail.productId !== quickAddCartId) return;
qty = event.detail.qty;
hasQty = qty > 0;
});
window.addEventListener('removeFromCart', (event) => {
if (hasMultipleVariants) {
if (!event.detail.productId.startsWith(productId + '-')) return;
delete variantQuantities[event.detail.productId];
recalcQtyFromVariants();
expanded = false;
loading = false;
pendingCollapse = false;
return;
}
if (event.detail.productId !== quickAddCartId) return;
if (suppressNextRemove) { suppressNextRemove = false; return; }
qty = 0;
hasQty = false;
expanded = false;
loading = false;
pendingCollapse = false;
});
">
Herrera
Chuncho Cacao
Chuncho Cacao
S/ 15.00
Edición Limitada
0 && !hasMultipleVariants) { expanded = true; pendingCollapse = false }"
@mouseleave="if (qty > 0 && !hasMultipleVariants) { if (loading) { pendingCollapse = true } else { expanded = false } }"
x-data="{
qty: 0,
hasQty: false,
maxQty: 50,
expanded: false,
loading: false,
pendingCollapse: false,
circleHover: false,
timer: null,
productId: 51,
quickAddCartId: '51',
hasMultipleVariants: false,
// Solo se usa con hasMultipleVariants -- mapea cartId real (id-variante)
// a qty, para poder sumar/restar por línea de carrito sin perder el
// conteo de las demás variantes ya agregadas cuando llega un evento de
// una sola de ellas. Ver el cast a objeto en el bloque @php de arriba.
variantQuantities: {},
// Evitan que el listener de window de más abajo vuelva a aplicar la propia
// mutación que esta misma instancia ya hizo localmente (Livewire.dispatch()
// llega también a esta instancia como evento de window, no solo a las demás).
suppressNextAdd: false,
suppressNextRemove: false,
// Suma todas las líneas de variantQuantities -- el número que ve el
// usuario en el círculo es cuántas unidades de este producto (en
// cualquier presentación) hay en el carrito, no de una variante puntual.
recalcQtyFromVariants() {
this.qty = Object.values(this.variantQuantities).reduce((sum, n) => sum + n, 0);
this.hasQty = this.qty > 0;
},
// window.dispatchEvent (NO Livewire.dispatch) a propósito -- el modal
// (variant-picker.blade.php) ya no es un componente Livewire que vuelve a
// consultar el producto al abrir; escucha este mismo evento de window y
// pinta con el payload que ya viaja acá, calculado una sola vez en el
// server al renderizar ESTA card (ver Product::quickViewPayload()). Así
// abre al instante, sin esperar ningún roundtrip -- Livewire.dispatch()
// sí implicaría uno (cualquier acción Livewire es una request).
//
// qtyInCart de cada variante SÍ se refresca acá antes de despachar --
// ese campo del payload es correcto recién al cargar la página, pero
// queda desactualizado si el cliente agrega/quita algo (de esta misma
// presentación o de otra) DESPUÉS, sin recargar (ej. reabre este mismo
// modal tras agregar una presentación una primera vez). variantQuantities
// de este círculo ya se mantiene sincronizado en vivo con el carrito real
// (ver los 3 listeners de window más abajo) -- se reusa esa MISMA fuente
// en vez de duplicarla, solo para pisar qtyInCart con el valor vigente
// justo antes de abrir.
openVariantPicker() {
const payload = null;
if (payload) {
payload.variants = payload.variants.map((variant) => ({
...variant,
qtyInCart: this.variantQuantities[this.productId + '-' + variant.name.replace(/ /g, '')] || 0,
}));
}
window.dispatchEvent(new CustomEvent('open-variant-picker', { detail: payload }));
},
addFirstTime() {
this.qty = 1;
this.hasQty = true;
this.expanded = true;
this.suppressNextAdd = true;
// dispatchId único por clic (no por instancia): deja que CartCounter
// descarte la segunda aplicación cuando sus 2 instancias montadas
// reciben este mismo evento -- ver nota en CartCounter::add().
const dispatchId = (window.crypto && crypto.randomUUID) ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
Livewire.dispatch('addToCart', { productId: this.quickAddCartId, qty: 1, dispatchId });
},
reopen() {
this.expanded = true;
},
changeQty(amount) {
// Tope real (ver maxQty arriba, Product::stockParaListado()): el botón
// de incrementar no debe pedir más de lo que hay -- respaldo del
// :disabled nativo (ver el markup más abajo), por si igual llega el
// click (estado de Alpine desfasado, DevTools). OJO al editar este
// comentario: todo este objeto x-data vive dentro de un atributo HTML
// entre comillas dobles, así que ninguna comilla doble literal puede
// aparecer acá adentro (ni en código ni en comentarios) -- cierra ese
// atributo antes de tiempo para el parser del navegador. BUG real ya
// encontrado y confirmado en vivo (screenshot: el resto del objeto
// x-data se pintaba como texto suelto encima de la card).
if (amount > 0 && this.qty >= this.maxQty) return;
const next = this.qty + amount;
clearTimeout(this.timer);
if (next < 1) {
this.timer = setTimeout(() => {
this.loading = true;
this.suppressNextRemove = true;
Livewire.dispatch('removeFromCart', { productId: this.quickAddCartId });
setTimeout(() => {
this.expanded = false; // inicia el leave del pill; loading sigue en true, el spinner tapa el número
setTimeout(() => {
this.hasQty = false; // circulo ya visible con el numero anterior; inicia su propio crossfade al icono mas
setTimeout(() => {
this.qty = 0;
this.loading = false;
this.pendingCollapse = false;
}, 150); // debe coincidir con la duración de x-transition.opacity.duration.150ms del span numérico
}, 250); // debe coincidir con la duración de product-card-add-pill-leave-active
}, 700);
}, 500);
return;
}
this.qty = next;
this.loading = true;
this.timer = setTimeout(() => {
Livewire.dispatch('updateCartQuantity', { productId: this.quickAddCartId, qty: this.qty });
setTimeout(() => {
this.loading = false;
if (this.pendingCollapse) {
this.expanded = false;
this.pendingCollapse = false;
}
}, 900);
}, 500);
}
}"
x-init="
$watch('expanded', (value) => {
if (value) { circleHover = false; }
$dispatch('cart-pill-toggle', { show: expanded || circleHover });
});
$watch('circleHover', () => $dispatch('cart-pill-toggle', { show: expanded || circleHover }));
// Mantiene sincronizado este botón cuando el carrito cambia desde otro
// lugar (drawer, VariantPicker, u otra instancia de esta misma card en
// otra sección de la página) -- Livewire.dispatch() llega a estos 3
// eventos como CustomEvent en window, con los mismos campos que ya usan
// CartCounter/CartDrawer (productId, qty).
//
// Con hasMultipleVariants, event.detail.productId es el cartId REAL
// con sufijo de variante (id-variante, ver VariantPicker::cartId() y
// VariantPicker::addToCart(), que dispara 'updateCartQuantity' al
// agregar desde el modal) -- nunca va a ser igual a quickAddCartId
// (que para estos productos es solo el id plano, sin variante, ver el
// bloque PHP de arriba). Por eso se compara por PREFIJO (id + guion)
// en vez de igualdad exacta, y se acumula por variante en
// variantQuantities en vez de pisar qty directo -- así una variante
// que cambia no borra el conteo de las demás ya agregadas.
window.addEventListener('addToCart', (event) => {
if (hasMultipleVariants) {
if (!event.detail.productId.startsWith(productId + '-')) return;
if (suppressNextAdd) { suppressNextAdd = false; return; }
variantQuantities[event.detail.productId] = (variantQuantities[event.detail.productId] || 0) + event.detail.qty;
recalcQtyFromVariants();
return;
}
if (event.detail.productId !== quickAddCartId) return;
if (suppressNextAdd) { suppressNextAdd = false; return; }
qty += event.detail.qty;
hasQty = qty > 0;
});
window.addEventListener('updateCartQuantity', (event) => {
if (hasMultipleVariants) {
if (!event.detail.productId.startsWith(productId + '-')) return;
variantQuantities[event.detail.productId] = event.detail.qty;
recalcQtyFromVariants();
return;
}
if (event.detail.productId !== quickAddCartId) return;
qty = event.detail.qty;
hasQty = qty > 0;
});
window.addEventListener('removeFromCart', (event) => {
if (hasMultipleVariants) {
if (!event.detail.productId.startsWith(productId + '-')) return;
delete variantQuantities[event.detail.productId];
recalcQtyFromVariants();
expanded = false;
loading = false;
pendingCollapse = false;
return;
}
if (event.detail.productId !== quickAddCartId) return;
if (suppressNextRemove) { suppressNextRemove = false; return; }
qty = 0;
hasQty = false;
expanded = false;
loading = false;
pendingCollapse = false;
});
">
Herrera
CCN-51 Cacao
CCN-51 Cacao
S/ 12.00
Edición Limitada
0 && !hasMultipleVariants) { expanded = true; pendingCollapse = false }"
@mouseleave="if (qty > 0 && !hasMultipleVariants) { if (loading) { pendingCollapse = true } else { expanded = false } }"
x-data="{
qty: 0,
hasQty: false,
maxQty: 50,
expanded: false,
loading: false,
pendingCollapse: false,
circleHover: false,
timer: null,
productId: 50,
quickAddCartId: '50',
hasMultipleVariants: false,
// Solo se usa con hasMultipleVariants -- mapea cartId real (id-variante)
// a qty, para poder sumar/restar por línea de carrito sin perder el
// conteo de las demás variantes ya agregadas cuando llega un evento de
// una sola de ellas. Ver el cast a objeto en el bloque @php de arriba.
variantQuantities: {},
// Evitan que el listener de window de más abajo vuelva a aplicar la propia
// mutación que esta misma instancia ya hizo localmente (Livewire.dispatch()
// llega también a esta instancia como evento de window, no solo a las demás).
suppressNextAdd: false,
suppressNextRemove: false,
// Suma todas las líneas de variantQuantities -- el número que ve el
// usuario en el círculo es cuántas unidades de este producto (en
// cualquier presentación) hay en el carrito, no de una variante puntual.
recalcQtyFromVariants() {
this.qty = Object.values(this.variantQuantities).reduce((sum, n) => sum + n, 0);
this.hasQty = this.qty > 0;
},
// window.dispatchEvent (NO Livewire.dispatch) a propósito -- el modal
// (variant-picker.blade.php) ya no es un componente Livewire que vuelve a
// consultar el producto al abrir; escucha este mismo evento de window y
// pinta con el payload que ya viaja acá, calculado una sola vez en el
// server al renderizar ESTA card (ver Product::quickViewPayload()). Así
// abre al instante, sin esperar ningún roundtrip -- Livewire.dispatch()
// sí implicaría uno (cualquier acción Livewire es una request).
//
// qtyInCart de cada variante SÍ se refresca acá antes de despachar --
// ese campo del payload es correcto recién al cargar la página, pero
// queda desactualizado si el cliente agrega/quita algo (de esta misma
// presentación o de otra) DESPUÉS, sin recargar (ej. reabre este mismo
// modal tras agregar una presentación una primera vez). variantQuantities
// de este círculo ya se mantiene sincronizado en vivo con el carrito real
// (ver los 3 listeners de window más abajo) -- se reusa esa MISMA fuente
// en vez de duplicarla, solo para pisar qtyInCart con el valor vigente
// justo antes de abrir.
openVariantPicker() {
const payload = null;
if (payload) {
payload.variants = payload.variants.map((variant) => ({
...variant,
qtyInCart: this.variantQuantities[this.productId + '-' + variant.name.replace(/ /g, '')] || 0,
}));
}
window.dispatchEvent(new CustomEvent('open-variant-picker', { detail: payload }));
},
addFirstTime() {
this.qty = 1;
this.hasQty = true;
this.expanded = true;
this.suppressNextAdd = true;
// dispatchId único por clic (no por instancia): deja que CartCounter
// descarte la segunda aplicación cuando sus 2 instancias montadas
// reciben este mismo evento -- ver nota en CartCounter::add().
const dispatchId = (window.crypto && crypto.randomUUID) ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
Livewire.dispatch('addToCart', { productId: this.quickAddCartId, qty: 1, dispatchId });
},
reopen() {
this.expanded = true;
},
changeQty(amount) {
// Tope real (ver maxQty arriba, Product::stockParaListado()): el botón
// de incrementar no debe pedir más de lo que hay -- respaldo del
// :disabled nativo (ver el markup más abajo), por si igual llega el
// click (estado de Alpine desfasado, DevTools). OJO al editar este
// comentario: todo este objeto x-data vive dentro de un atributo HTML
// entre comillas dobles, así que ninguna comilla doble literal puede
// aparecer acá adentro (ni en código ni en comentarios) -- cierra ese
// atributo antes de tiempo para el parser del navegador. BUG real ya
// encontrado y confirmado en vivo (screenshot: el resto del objeto
// x-data se pintaba como texto suelto encima de la card).
if (amount > 0 && this.qty >= this.maxQty) return;
const next = this.qty + amount;
clearTimeout(this.timer);
if (next < 1) {
this.timer = setTimeout(() => {
this.loading = true;
this.suppressNextRemove = true;
Livewire.dispatch('removeFromCart', { productId: this.quickAddCartId });
setTimeout(() => {
this.expanded = false; // inicia el leave del pill; loading sigue en true, el spinner tapa el número
setTimeout(() => {
this.hasQty = false; // circulo ya visible con el numero anterior; inicia su propio crossfade al icono mas
setTimeout(() => {
this.qty = 0;
this.loading = false;
this.pendingCollapse = false;
}, 150); // debe coincidir con la duración de x-transition.opacity.duration.150ms del span numérico
}, 250); // debe coincidir con la duración de product-card-add-pill-leave-active
}, 700);
}, 500);
return;
}
this.qty = next;
this.loading = true;
this.timer = setTimeout(() => {
Livewire.dispatch('updateCartQuantity', { productId: this.quickAddCartId, qty: this.qty });
setTimeout(() => {
this.loading = false;
if (this.pendingCollapse) {
this.expanded = false;
this.pendingCollapse = false;
}
}, 900);
}, 500);
}
}"
x-init="
$watch('expanded', (value) => {
if (value) { circleHover = false; }
$dispatch('cart-pill-toggle', { show: expanded || circleHover });
});
$watch('circleHover', () => $dispatch('cart-pill-toggle', { show: expanded || circleHover }));
// Mantiene sincronizado este botón cuando el carrito cambia desde otro
// lugar (drawer, VariantPicker, u otra instancia de esta misma card en
// otra sección de la página) -- Livewire.dispatch() llega a estos 3
// eventos como CustomEvent en window, con los mismos campos que ya usan
// CartCounter/CartDrawer (productId, qty).
//
// Con hasMultipleVariants, event.detail.productId es el cartId REAL
// con sufijo de variante (id-variante, ver VariantPicker::cartId() y
// VariantPicker::addToCart(), que dispara 'updateCartQuantity' al
// agregar desde el modal) -- nunca va a ser igual a quickAddCartId
// (que para estos productos es solo el id plano, sin variante, ver el
// bloque PHP de arriba). Por eso se compara por PREFIJO (id + guion)
// en vez de igualdad exacta, y se acumula por variante en
// variantQuantities en vez de pisar qty directo -- así una variante
// que cambia no borra el conteo de las demás ya agregadas.
window.addEventListener('addToCart', (event) => {
if (hasMultipleVariants) {
if (!event.detail.productId.startsWith(productId + '-')) return;
if (suppressNextAdd) { suppressNextAdd = false; return; }
variantQuantities[event.detail.productId] = (variantQuantities[event.detail.productId] || 0) + event.detail.qty;
recalcQtyFromVariants();
return;
}
if (event.detail.productId !== quickAddCartId) return;
if (suppressNextAdd) { suppressNextAdd = false; return; }
qty += event.detail.qty;
hasQty = qty > 0;
});
window.addEventListener('updateCartQuantity', (event) => {
if (hasMultipleVariants) {
if (!event.detail.productId.startsWith(productId + '-')) return;
variantQuantities[event.detail.productId] = event.detail.qty;
recalcQtyFromVariants();
return;
}
if (event.detail.productId !== quickAddCartId) return;
qty = event.detail.qty;
hasQty = qty > 0;
});
window.addEventListener('removeFromCart', (event) => {
if (hasMultipleVariants) {
if (!event.detail.productId.startsWith(productId + '-')) return;
delete variantQuantities[event.detail.productId];
recalcQtyFromVariants();
expanded = false;
loading = false;
pendingCollapse = false;
return;
}
if (event.detail.productId !== quickAddCartId) return;
if (suppressNextRemove) { suppressNextRemove = false; return; }
qty = 0;
hasQty = false;
expanded = false;
loading = false;
pendingCollapse = false;
});
">