feat(hero-banner): update image handling to support responsive images and refactor template

feat(cart): enhance editing logic and add tests for editable state
feat(tenant): modify HeroConfig to include responsive image type
This commit is contained in:
2026-08-18 14:18:20 -03:00
parent 92ad7340fe
commit 9ba2041f7b
9 changed files with 115 additions and 14 deletions

View File

@@ -20,13 +20,18 @@ export interface WebsiteType {
} }
export interface HeroConfig { export interface HeroConfig {
background_image_id?: string | null; background_image_id?: string | ResponsiveImage | null;
title_html?: string | null; title_html?: string | null;
description_html?: string | null; description_html?: string | null;
button_text?: string | null; button_text?: string | null;
button_href?: string | null; button_href?: string | null;
} }
export interface ResponsiveImage {
desktop: string;
mobile: string;
}
export interface AdditionalInfoConfig { export interface AdditionalInfoConfig {
description?: string | null; description?: string | null;
} }

View File

@@ -254,9 +254,10 @@ describe('StoreHomePageComponent', () => {
const hero = (fixture.nativeElement as HTMLElement).querySelector( const hero = (fixture.nativeElement as HTMLElement).querySelector(
'app-hero-banner .hero-banner', 'app-hero-banner .hero-banner',
) as HTMLElement; ) as HTMLElement;
const heroImage = hero.querySelector('img');
expect(hero).not.toBeNull(); expect(hero).not.toBeNull();
expect(hero.style.backgroundImage).toContain('https://example.com/hero.jpg'); expect(heroImage?.getAttribute('src')).toBe('https://example.com/hero.jpg');
expect(hero.textContent).toContain('Fiesta Fútbol Infantil'); expect(hero.textContent).toContain('Fiesta Fútbol Infantil');
expect(hero.textContent).toContain('Viví una jornada inolvidable de fútbol infantil.'); expect(hero.textContent).toContain('Viví una jornada inolvidable de fútbol infantil.');
expect(hero.textContent).toContain('Rosario, Santa Fe'); expect(hero.textContent).toContain('Rosario, Santa Fe');

View File

@@ -6,7 +6,7 @@
<h2 class="m-0 text-uppercase fw-bold cart-title">{{ title() }}</h2> <h2 class="m-0 text-uppercase fw-bold cart-title">{{ title() }}</h2>
<div class="d-flex align-items-center cart-header-actions"> <div class="d-flex align-items-center cart-header-actions">
@if (!readonly() && allowEditing() && items().length > 0) { @if (!readonly() && editable() && allowEditing() && items().length > 0) {
<button <button
class="btn btn-link p-0 border-0 cart-edit-btn" class="btn btn-link p-0 border-0 cart-edit-btn"
type="button" type="button"
@@ -48,7 +48,7 @@
[selectedVariant]="getItemVariant(item)" [selectedVariant]="getItemVariant(item)"
[quantity]="getItemQuantity(item)" [quantity]="getItemQuantity(item)"
[readonly]="readonly()" [readonly]="readonly()"
[quantityDisabled]="editingDisabled() || (allowEditing() && !editing())" [quantityDisabled]="!editable() || editingDisabled() || (allowEditing() && !editing())"
[showRemove]="allowRemove()" [showRemove]="allowRemove()"
(quantityChange)="onItemQuantityChange(idx, $event)" (quantityChange)="onItemQuantityChange(idx, $event)"
(variantChange)="onItemVariantChange(idx, $event)" (variantChange)="onItemVariantChange(idx, $event)"

View File

@@ -408,6 +408,54 @@ describe('CartComponent', () => {
).toBe(false); ).toBe(false);
}); });
it('hides the edit toggle and disables quantity changes when editable is false', async () => {
await TestBed.configureTestingModule({
imports: [CartComponent],
providers: [
{
provide: CartService,
useValue: {
cart: signal(null).asReadonly(),
updateItemQuantity: vi.fn(),
removeItem: vi.fn(),
},
},
{ provide: ModalService, useValue: {} },
{
provide: ToastService,
useValue: { success: vi.fn(), info: vi.fn(), danger: vi.fn() },
},
],
}).compileComponents();
const fixture = TestBed.createComponent(CartComponent);
fixture.componentRef.setInput('items', [
{
cartItemId: 10,
imageUrl: null,
product: 'Producto de prueba',
originalPrice: null,
discountedPrice: 1000,
discountPercentage: null,
attributes: [],
quantity: 1,
},
]);
fixture.componentRef.setInput('allowEditing', true);
fixture.componentRef.setInput('editable', false);
const quantityChange = vi.fn();
fixture.componentInstance.itemQuantityChange.subscribe(quantityChange);
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.cart-edit-btn'))).toBeNull();
const cartItem = fixture.debugElement.query(By.css('app-cart-item'));
expect(cartItem.componentInstance.quantityDisabled()).toBe(true);
cartItem.triggerEventHandler('quantityChange', 2);
expect(quantityChange).not.toHaveBeenCalled();
});
it('persists a variant selected from a cart row', async () => { it('persists a variant selected from a cart row', async () => {
const updateItemVariant = vi.fn().mockReturnValue( const updateItemVariant = vi.fn().mockReturnValue(
of({ of({

View File

@@ -53,6 +53,7 @@ export class CartComponent {
readonly total = input<number>(0); readonly total = input<number>(0);
readonly backgroundColor = input<string>('#ffffff'); readonly backgroundColor = input<string>('#ffffff');
readonly readonly = input<boolean>(false); readonly readonly = input<boolean>(false);
readonly editable = input<boolean>(true);
readonly allowEditing = input<boolean>(false); readonly allowEditing = input<boolean>(false);
readonly allowRemove = input<boolean>(true); readonly allowRemove = input<boolean>(true);
readonly persistQuantityChanges = input<boolean>(true); readonly persistQuantityChanges = input<boolean>(true);
@@ -124,6 +125,10 @@ export class CartComponent {
} }
protected onItemQuantityChange(index: number, newQuantity: number): void { protected onItemQuantityChange(index: number, newQuantity: number): void {
if (!this.editable()) {
return;
}
const mockItem = this.items()[index]; const mockItem = this.items()[index];
if (!mockItem) { if (!mockItem) {
return; return;
@@ -220,7 +225,7 @@ export class CartComponent {
protected readonly formattedTotal = computed(() => this.formatCurrency(this.total())); protected readonly formattedTotal = computed(() => this.formatCurrency(this.total()));
protected toggleEditing(): void { protected toggleEditing(): void {
if (this.editingDisabled()) { if (!this.editable() || this.editingDisabled()) {
return; return;
} }

View File

@@ -1,14 +1,13 @@
<div class="hero-banner-container"> <div class="hero-banner-container">
<div class="hero-banner position-relative rounded"> <div class="hero-banner position-relative rounded">
<div @if (desktopImageUrl) {
class="hero-media" <picture class="hero-media" aria-hidden="true">
aria-hidden="true" @if (mobileImageUrl) {
[ngStyle]="{ <source media="(max-width: 767.98px)" [srcset]="mobileImageUrl" />
'background-image': heroConfig?.background_image_id }
? 'url(' + heroConfig.background_image_id + ')' <img [src]="desktopImageUrl" alt="" />
: 'none', </picture>
}" }
></div>
@if (heroConfig) { @if (heroConfig) {
<div <div

View File

@@ -12,11 +12,20 @@
.hero-media { .hero-media {
position: absolute; position: absolute;
inset: 0; inset: 0;
display: block;
overflow: hidden;
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
border-radius: inherit; border-radius: inherit;
img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
&::after { &::after {
content: ''; content: '';
position: absolute; position: absolute;

View File

@@ -2,6 +2,28 @@ import { TestBed } from '@angular/core/testing';
import { HeroBannerComponent } from './hero-banner.component'; import { HeroBannerComponent } from './hero-banner.component';
describe('HeroBannerComponent', () => { describe('HeroBannerComponent', () => {
it('renders desktop and mobile crop variants', async () => {
await TestBed.configureTestingModule({ imports: [HeroBannerComponent] }).compileComponents();
const fixture = TestBed.createComponent(HeroBannerComponent);
fixture.componentRef.setInput('heroConfig', {
background_image_id: {
desktop: 'https://example.com/desktop.jpg',
mobile: 'https://example.com/mobile.jpg',
},
});
fixture.detectChanges();
const element = fixture.nativeElement as HTMLElement;
expect(element.querySelector('source')?.getAttribute('srcset')).toBe(
'https://example.com/mobile.jpg',
);
expect(element.querySelector('img')?.getAttribute('src')).toBe(
'https://example.com/desktop.jpg',
);
});
it('expands and collapses the event schedules', async () => { it('expands and collapses the event schedules', async () => {
await TestBed.configureTestingModule({ imports: [HeroBannerComponent] }).compileComponents(); await TestBed.configureTestingModule({ imports: [HeroBannerComponent] }).compileComponents();

View File

@@ -18,6 +18,18 @@ export class HeroBannerComponent {
protected schedulesExpanded = false; protected schedulesExpanded = false;
protected get desktopImageUrl(): string | null {
const image = this.heroConfig?.background_image_id;
return typeof image === 'string' ? image : (image?.desktop ?? null);
}
protected get mobileImageUrl(): string | null {
const image = this.heroConfig?.background_image_id;
return typeof image === 'string' ? image : (image?.mobile ?? image?.desktop ?? null);
}
get formattedDates(): string { get formattedDates(): string {
if (this.eventConfig?.dates_text) { if (this.eventConfig?.dates_text) {
return this.eventConfig.dates_text; return this.eventConfig.dates_text;