feat: implement product attribute selector component and integrate into product detail page

This commit is contained in:
2026-07-01 10:48:05 -03:00
parent 1a7a6c28e6
commit 3ad29f959d
5 changed files with 71 additions and 2 deletions

View File

@@ -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 }}

View File

@@ -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;
}
}
}

View File

@@ -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();

View File

@@ -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>

View File

@@ -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 {