-
Talle:
-
-
- @for (option of sizeAttr.options; track option.id) {
-
+
+ @for (option of attribute.options; track option.id) {
+ @if (isColorOption(option)) {
+
+ } @else {
+
+ }
}
diff --git a/src/app/features/store/pages/product-detail-page/product-detail-page.component.scss b/src/app/features/store/pages/product-detail-page/product-detail-page.component.scss
index 4dc0a3a..d2f34f6 100644
--- a/src/app/features/store/pages/product-detail-page/product-detail-page.component.scss
+++ b/src/app/features/store/pages/product-detail-page/product-detail-page.component.scss
@@ -19,11 +19,15 @@
&__title {
font-size: 28px;
- font-weight: 325;
+ font-weight: 400;
line-height: 1.35;
color: #000000;
}
+ &__title-price-divider {
+ border-top: 1px solid #e6e6e6;
+ }
+
&__price-group {
display: flex;
align-items: baseline;
@@ -33,7 +37,7 @@
&__price-current {
font-size: 42px;
- font-weight: 425;
+ font-weight: 700;
line-height: 1.1;
color: var(--tenant-primary);
}
@@ -59,14 +63,13 @@
&__attribute-label {
font-size: 15px;
- font-weight: 400;
+ font-weight: 700;
line-height: 1.2;
- color: #666666;
+ color: #A0A0A0;
min-width: 44px;
}
- &__color-options,
- &__size-options,
+ &__attribute-options,
&__actions {
display: flex;
align-items: center;
@@ -74,7 +77,7 @@
flex-wrap: wrap;
}
- &__color-swatch {
+ &__attribute-swatch {
width: 22px;
height: 22px;
border-radius: 50%;
@@ -96,7 +99,7 @@
}
}
- &__size-option,
+ &__attribute-text-option,
&__quantity-button {
border: 1px solid #dcdcdc;
background: #ffffff;
@@ -104,13 +107,13 @@
transition: border-color 0.2s ease, color 0.2s ease, background-color 0.2s ease;
}
- &__size-option {
+ &__attribute-text-option {
min-width: 34px;
min-height: 34px;
border-radius: 4px;
padding: 0.35rem 0.6rem;
- font-size: 15px;
- font-weight: 400;
+ font-size: 16px;
+ font-weight: 700;
line-height: 1;
&--selected {
@@ -130,28 +133,34 @@
&__quantity {
display: inline-flex;
align-items: center;
- border: 1px solid #dcdcdc;
- border-radius: 4px;
+ border: 1px solid #d8d8d8;
+ border-radius: 6px;
overflow: hidden;
- min-height: 42px;
+ min-height: 44px;
+ background: #f6f6f6;
}
&__quantity-button {
- width: 34px;
- height: 42px;
+ width: 25px;
+ height: 44px;
padding: 0;
- font-size: 18px;
- font-weight: 400;
+ font-size: 20px;
+ font-weight: 700;
line-height: 1;
+ color: #6f6f6f;
+ background: #f6f6f6;
+ border: 0;
+
+
}
&__quantity-value {
- min-width: 40px;
+ min-width: 35px;
text-align: center;
- font-size: 15px;
- font-weight: 400;
+ font-size: 16px;
+ font-weight: 700;
line-height: 1;
- color: #666666;
+ color: #6f6f6f;
}
&__actions {
diff --git a/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts b/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts
index 2677943..6ac07c1 100644
--- a/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts
+++ b/src/app/features/store/pages/product-detail-page/product-detail-page.component.spec.ts
@@ -155,7 +155,7 @@ describe('ProductDetailPageComponent', () => {
expect(carousel.images()).toEqual([]);
});
- it('renders color and size selectors and preselects default variant options', async () => {
+ it('renders generic attributes and preselects default variant options', async () => {
const detailProduct: ProductDetail = {
...mockProduct,
attributes: [
@@ -185,23 +185,23 @@ describe('ProductDetailPageComponent', () => {
},
{
id: 2,
- codigo: 'talle',
- nombre: 'Talle',
+ codigo: 'material',
+ nombre: 'Material',
is_required: true,
metadata_schema: null,
type: 'select',
options: [
{
id: 20,
- value: 'S',
- label: 'S',
+ value: 'mesh',
+ label: 'Mesh',
sort_order: 1,
metadata: null
},
{
id: 21,
- value: 'M',
- label: 'M',
+ value: 'cuero',
+ label: 'Cuero',
sort_order: 2,
metadata: null
}
@@ -213,7 +213,7 @@ describe('ProductDetailPageComponent', () => {
images: ['https://example.com/variant1.png'],
attributes: {
color: 'beige',
- talle: 'M'
+ material: 'Cuero'
}
}
};
@@ -224,14 +224,29 @@ describe('ProductDetailPageComponent', () => {
fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement;
- const colorOptions = element.querySelectorAll('.product-detail__color-swatch');
- const sizeOptions = element.querySelectorAll('.product-detail__size-option');
+ const swatches = element.querySelectorAll('.product-detail__attribute-swatch');
+ const textOptions = element.querySelectorAll('.product-detail__attribute-text-option');
+ const labels = Array.from(element.querySelectorAll('.product-detail__attribute-label')).map((label) =>
+ label.textContent?.trim()
+ );
- expect(colorOptions).toHaveLength(2);
- expect(sizeOptions).toHaveLength(2);
- expect(colorOptions[0].classList.contains('product-detail__color-swatch--selected')).toBe(true);
- expect(sizeOptions[1].classList.contains('product-detail__size-option--selected')).toBe(true);
- expect((colorOptions[0] as HTMLElement).style.backgroundColor).not.toBe('');
+ expect(labels).toEqual(['Color:', 'Material:']);
+ expect(swatches).toHaveLength(2);
+ expect(textOptions).toHaveLength(2);
+ expect(swatches[0].classList.contains('product-detail__attribute-swatch--selected')).toBe(true);
+ expect(textOptions[1].classList.contains('product-detail__attribute-text-option--selected')).toBe(true);
+ expect((swatches[0] as HTMLElement).style.backgroundColor).not.toBe('');
+ });
+
+ it('hides the attributes block and its extra divider when no attributes are present', async () => {
+ await configureTestingModule();
+ const fixture = TestBed.createComponent(ProductDetailPageComponent);
+ fixture.detectChanges();
+
+ const element = fixture.nativeElement as HTMLElement;
+
+ expect(element.querySelector('.product-detail__section--attributes')).toBeNull();
+ expect(element.querySelectorAll('.product-detail__divider')).toHaveLength(2);
});
it('keeps quantity at a minimum of one and increments locally', async () => {
diff --git a/src/app/features/store/pages/product-detail-page/product-detail-page.component.ts b/src/app/features/store/pages/product-detail-page/product-detail-page.component.ts
index b32c6a0..3bed3d4 100644
--- a/src/app/features/store/pages/product-detail-page/product-detail-page.component.ts
+++ b/src/app/features/store/pages/product-detail-page/product-detail-page.component.ts
@@ -59,17 +59,16 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
});
protected readonly loading = signal(false);
protected readonly error = signal
(null);
- protected readonly selectedColor = signal(null);
- protected readonly selectedSize = signal(null);
+ protected readonly selectedAttributeOptions = signal>({});
protected readonly quantity = signal(1);
protected readonly descriptionExpanded = signal(false);
protected readonly descriptionMaxHeight = signal(0);
protected readonly descriptionHasOverflow = signal(false);
- protected readonly colorAttribute = computed(() =>
- this.findAttribute(this.product()?.attributes ?? [], ['color', 'colour'])
+ protected readonly renderableAttributes = computed(() =>
+ (this.product()?.attributes ?? []).filter((attribute) => attribute.options.length > 0)
);
- protected readonly sizeAttribute = computed(() =>
- this.findAttribute(this.product()?.attributes ?? [], ['talle', 'talla', 'size'])
+ protected readonly hasRenderableAttributes = computed(
+ () => this.renderableAttributes().length > 0
);
protected readonly oldPrice = computed(() => null);
protected readonly showDescriptionToggle = computed(
@@ -147,20 +146,22 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
return `$${parts.join(',')}`;
}
- protected hasColorSelection(optionId: number): boolean {
- return this.selectedColor() === optionId;
+ protected hasSelectedOption(attribute: ProductAttribute, option: ProductAttributeOption): boolean {
+ return this.selectedAttributeOptions()[attribute.id] === option.id;
}
- protected hasSizeSelection(optionId: number): boolean {
- return this.selectedSize() === optionId;
+ protected selectAttributeOption(
+ attribute: ProductAttribute,
+ option: ProductAttributeOption
+ ): void {
+ this.selectedAttributeOptions.update((current) => ({
+ ...current,
+ [attribute.id]: option.id
+ }));
}
- protected selectColor(optionId: number): void {
- this.selectedColor.set(optionId);
- }
-
- protected selectSize(optionId: number): void {
- this.selectedSize.set(optionId);
+ protected isColorOption(option: ProductAttributeOption): boolean {
+ return this.findFirstHexValue(option.metadata) !== null;
}
protected increaseQuantity(): void {
@@ -180,19 +181,24 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
}
private initializeSelections(product: ProductDetail): void {
- const colorAttribute = this.findAttribute(product.attributes, ['color', 'colour']);
- const sizeAttribute = this.findAttribute(product.attributes, ['talle', 'talla', 'size']);
const defaultVariant = product.default_variant;
+ const selections: Record = {};
- this.selectedColor.set(this.findDefaultOptionId(colorAttribute, defaultVariant));
- this.selectedSize.set(this.findDefaultOptionId(sizeAttribute, defaultVariant));
+ for (const attribute of product.attributes) {
+ const optionId = this.findDefaultOptionId(attribute, defaultVariant);
+ if (optionId !== null) {
+ selections[attribute.id] = optionId;
+ }
+ }
+
+ this.selectedAttributeOptions.set(selections);
}
private findDefaultOptionId(
- attribute: ProductAttribute | undefined,
+ attribute: ProductAttribute,
variant: ProductVariant | null
): number | null {
- if (!attribute || !variant) {
+ if (!variant) {
return null;
}
@@ -234,26 +240,6 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
return null;
}
-
- private findAttribute(
- attributes: ProductAttribute[],
- aliases: string[]
- ): ProductAttribute | undefined {
- const normalizedAliases = aliases.map((alias) => this.normalizeText(alias));
-
- const codeMatch = attributes.find((attribute) =>
- normalizedAliases.includes(this.normalizeText(attribute.codigo))
- );
-
- if (codeMatch) {
- return codeMatch;
- }
-
- return attributes.find((attribute) =>
- normalizedAliases.includes(this.normalizeText(attribute.nombre))
- );
- }
-
private normalizeText(value: string | null | undefined): string {
return (value ?? '')
.normalize('NFD')