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:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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)"
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user