feat: implement product attribute selector component and integrate into product detail page
This commit is contained in:
@@ -10,10 +10,12 @@
|
||||
type="button"
|
||||
class="attribute-selector__swatch"
|
||||
[class.attribute-selector__swatch--selected]="hasSelectedOption(attribute, option)"
|
||||
[class.attribute-selector__swatch--disabled]="!availableOptions()[attribute.id][option.id]"
|
||||
[style.background-color]="getOptionSwatchColor(option)"
|
||||
[attr.aria-label]="option.label"
|
||||
[attr.aria-pressed]="hasSelectedOption(attribute, option)"
|
||||
[title]="option.label"
|
||||
[disabled]="!availableOptions()[attribute.id][option.id]"
|
||||
(click)="selectAttributeOption(attribute, option)"
|
||||
>
|
||||
<span class="visually-hidden">{{ option.label }}</span>
|
||||
@@ -23,7 +25,9 @@
|
||||
type="button"
|
||||
class="attribute-selector__text-option"
|
||||
[class.attribute-selector__text-option--selected]="hasSelectedOption(attribute, option)"
|
||||
[class.attribute-selector__text-option--disabled]="!availableOptions()[attribute.id][option.id]"
|
||||
[attr.aria-pressed]="hasSelectedOption(attribute, option)"
|
||||
[disabled]="!availableOptions()[attribute.id][option.id]"
|
||||
(click)="selectAttributeOption(attribute, option)"
|
||||
>
|
||||
{{ option.label }}
|
||||
|
||||
@@ -45,6 +45,12 @@
|
||||
0 0 0 2px rgba(255, 255, 255, 1),
|
||||
0 0 0 3px var(--tenant-primary);
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__text-option {
|
||||
@@ -65,5 +71,13 @@
|
||||
border-color: var(--tenant-primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
background-color: #f9f9f9;
|
||||
color: #b0b0b0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
input,
|
||||
output,
|
||||
@@ -32,6 +33,50 @@ export class ProductAttributeSelectorComponent {
|
||||
|
||||
protected readonly selectedAttributeOptions = signal<Record<number, number>>({});
|
||||
|
||||
protected readonly availableOptions = computed(() => {
|
||||
const selections = this.selectedAttributeOptions();
|
||||
const variants = this.variantsMap();
|
||||
const attributes = this.attributes();
|
||||
|
||||
const availability: Record<number, Record<number, boolean>> = {};
|
||||
|
||||
for (const attribute of attributes) {
|
||||
availability[attribute.id] = {};
|
||||
for (const option of attribute.options) {
|
||||
const optionNormalized = this.normalizeText(option.value || option.label);
|
||||
|
||||
const isAvailable = variants.some(variant => {
|
||||
if (variant.stock <= 0) return false;
|
||||
|
||||
const variantAttrValue = this.getDefaultVariantAttributeValue(attribute, variant.attributes);
|
||||
if (!variantAttrValue || this.normalizeText(variantAttrValue) !== optionNormalized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const otherAttr of attributes) {
|
||||
if (otherAttr.id === attribute.id) continue;
|
||||
const selectedOptionId = selections[otherAttr.id];
|
||||
if (selectedOptionId !== undefined) {
|
||||
const selectedOption = otherAttr.options.find(o => o.id === selectedOptionId);
|
||||
if (selectedOption) {
|
||||
const selectedNormalized = this.normalizeText(selectedOption.value || selectedOption.label);
|
||||
const vAttrValue = this.getDefaultVariantAttributeValue(otherAttr, variant.attributes);
|
||||
if (!vAttrValue || this.normalizeText(vAttrValue) !== selectedNormalized) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
availability[attribute.id][option.id] = isAvailable;
|
||||
}
|
||||
}
|
||||
|
||||
return availability;
|
||||
});
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const attributes = this.attributes();
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
type="button"
|
||||
class="product-detail__quantity-button"
|
||||
aria-label="Aumentar cantidad"
|
||||
[disabled]="quantity() >= (selectedVariant()?.stock ?? 1)"
|
||||
(click)="increaseQuantity()"
|
||||
>
|
||||
+
|
||||
@@ -80,11 +81,12 @@
|
||||
class="product-detail__cta"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
[disabled]="!selectedVariant()"
|
||||
>
|
||||
Agregar al carrito
|
||||
</app-button>
|
||||
|
||||
<app-button class="product-detail__cta" type="button">
|
||||
<app-button class="product-detail__cta" type="button" [disabled]="!selectedVariant()">
|
||||
Comprar
|
||||
</app-button>
|
||||
</div>
|
||||
|
||||
@@ -154,10 +154,14 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
|
||||
|
||||
protected onVariantChange(variant: ProductVariantMap | null): void {
|
||||
this.selectedVariant.set(variant);
|
||||
if (variant && this.quantity() > variant.stock) {
|
||||
this.quantity.set(Math.max(1, variant.stock));
|
||||
}
|
||||
}
|
||||
|
||||
protected increaseQuantity(): void {
|
||||
this.quantity.update((current) => current + 1);
|
||||
const currentStock = this.selectedVariant()?.stock ?? 1;
|
||||
this.quantity.update((current) => current < currentStock ? current + 1 : current);
|
||||
}
|
||||
|
||||
protected decreaseQuantity(): void {
|
||||
|
||||
Reference in New Issue
Block a user