feat(ticket-selector): implement zoomable image feature for seating map
This commit is contained in:
parent
f0c1e74c1b
commit
ebae063a0f
|
|
@ -107,6 +107,17 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (imageUrl(); as image) {
|
@if (imageUrl(); as image) {
|
||||||
<img class="ticket-selector__map" [src]="image" [alt]="'Plano de ubicaciones de ' + title()" />
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ticket-selector__map-button"
|
||||||
|
aria-label="Ampliar plano de ubicaciones"
|
||||||
|
(click)="openImage(image)"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="ticket-selector__map"
|
||||||
|
[src]="image"
|
||||||
|
[alt]="'Plano de ubicaciones de ' + title()"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
}
|
}
|
||||||
</article>
|
</article>
|
||||||
|
|
|
||||||
|
|
@ -153,11 +153,27 @@
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__map {
|
&__map-button {
|
||||||
display: block;
|
display: block;
|
||||||
width: min(100%, 720px);
|
width: min(100%, 720px);
|
||||||
max-height: 680px;
|
|
||||||
margin: 3rem auto 0;
|
margin: 3rem auto 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
background: transparent;
|
||||||
|
cursor: zoom-in;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 3px solid var(--tenant-primary);
|
||||||
|
outline-offset: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__map {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 680px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -188,7 +204,7 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__map {
|
&__map-button {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,10 @@ describe('ProductTicketSelectorComponent', () => {
|
||||||
};
|
};
|
||||||
catalogService.withoutLoading.mockReturnValue(catalogService);
|
catalogService.withoutLoading.mockReturnValue(catalogService);
|
||||||
cartService.withoutLoading.mockReturnValue(cartService);
|
cartService.withoutLoading.mockReturnValue(cartService);
|
||||||
|
const modalService = {
|
||||||
|
openConfirmDelete: vi.fn().mockReturnValue(of(true)),
|
||||||
|
openImage: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [ProductTicketSelectorComponent],
|
imports: [ProductTicketSelectorComponent],
|
||||||
|
|
@ -112,7 +116,7 @@ describe('ProductTicketSelectorComponent', () => {
|
||||||
{ provide: CartService, useValue: cartService },
|
{ provide: CartService, useValue: cartService },
|
||||||
{
|
{
|
||||||
provide: ModalService,
|
provide: ModalService,
|
||||||
useValue: { openConfirmDelete: vi.fn().mockReturnValue(of(true)) },
|
useValue: modalService,
|
||||||
},
|
},
|
||||||
{ provide: ToastService, useValue: { danger: toastDanger } },
|
{ provide: ToastService, useValue: { danger: toastDanger } },
|
||||||
],
|
],
|
||||||
|
|
@ -125,7 +129,7 @@ describe('ProductTicketSelectorComponent', () => {
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
return { fixture, getVariantOptions, cartService, toastDanger };
|
return { fixture, getVariantOptions, cartService, toastDanger, modalService };
|
||||||
}
|
}
|
||||||
|
|
||||||
it('loads one map, recalculates the cascade locally and reserves only after the last select', async () => {
|
it('loads one map, recalculates the cascade locally and reserves only after the last select', async () => {
|
||||||
|
|
@ -226,6 +230,25 @@ describe('ProductTicketSelectorComponent', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('opens the zoomable image modal when the seating map is clicked', async () => {
|
||||||
|
const { fixture, modalService } = await createComponent({
|
||||||
|
maps: [mapResponse([variant(401, 'general', 'A', '1', '1')])],
|
||||||
|
});
|
||||||
|
fixture.componentRef.setInput('imageUrl', '/images/plano.png');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const button = fixture.nativeElement.querySelector(
|
||||||
|
'button[aria-label="Ampliar plano de ubicaciones"]',
|
||||||
|
) as HTMLButtonElement;
|
||||||
|
button.click();
|
||||||
|
|
||||||
|
expect(modalService.openImage).toHaveBeenCalledWith({
|
||||||
|
title: 'Entrada',
|
||||||
|
src: '/images/plano.png',
|
||||||
|
alt: 'Plano de ubicaciones de Entrada',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('clears only the seat after a stock conflict when the row still has alternatives', async () => {
|
it('clears only the seat after a stock conflict when the row still has alternatives', async () => {
|
||||||
const failed = variant(401, 'general', 'A', '1', '1');
|
const failed = variant(401, 'general', 'A', '1', '1');
|
||||||
const alternative = variant(402, 'general', 'A', '1', '2');
|
const alternative = variant(402, 'general', 'A', '1', '2');
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,14 @@ export class ProductTicketSelectorComponent {
|
||||||
if (this.hasSelection() && variantIds.length > 0) this.buy.emit(variantIds);
|
if (this.hasSelection() && variantIds.length > 0) this.buy.emit(variantIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected openImage(src: string): void {
|
||||||
|
this.modalService.openImage({
|
||||||
|
title: this.title(),
|
||||||
|
src,
|
||||||
|
alt: `Plano de ubicaciones de ${this.title()}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected addRow(): void {
|
protected addRow(): void {
|
||||||
if (!this.canAddRow()) return;
|
if (!this.canAddRow()) return;
|
||||||
const row = this.createRow(this.nextRowId++);
|
const row = this.createRow(this.nextRowId++);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue