diff --git a/src/app/core/layout/store-layout/store-header/store-header.component.html b/src/app/core/layout/store-layout/store-header/store-header.component.html index 975deca..189ede0 100644 --- a/src/app/core/layout/store-layout/store-header/store-header.component.html +++ b/src/app/core/layout/store-layout/store-header/store-header.component.html @@ -79,10 +79,13 @@ @if (displayCart()) { } diff --git a/src/app/core/layout/store-layout/store-header/store-header.component.ts b/src/app/core/layout/store-layout/store-header/store-header.component.ts index 3edd038..9d7723b 100644 --- a/src/app/core/layout/store-layout/store-header/store-header.component.ts +++ b/src/app/core/layout/store-layout/store-header/store-header.component.ts @@ -39,6 +39,7 @@ export class StoreHeaderComponent { readonly displayCategories = input(true); readonly displaySeachBar = input(true); readonly displayCart = input(true); + readonly cartDisabled = input(false); readonly cartClick = output(); readonly ticketsClick = output(); readonly loginClick = output(); @@ -53,6 +54,14 @@ export class StoreHeaderComponent { protected readonly showSearchError = signal(false); protected readonly searchControl = new FormControl('', { nonNullable: true }); + protected onCartClick(): void { + if (this.cartDisabled()) { + return; + } + + this.cartClick.emit(); + } + @HostListener('document:click', ['$event']) protected onDocumentClick(event: MouseEvent): void { if (!this.isUserDropdownOpen() && !this.isCategoryDropdownOpen() && !this.isMobileMenuOpen()) { diff --git a/src/app/core/layout/store-layout/store-layout.component.html b/src/app/core/layout/store-layout/store-layout.component.html index d37b270..896168b 100644 --- a/src/app/core/layout/store-layout/store-layout.component.html +++ b/src/app/core/layout/store-layout/store-layout.component.html @@ -12,7 +12,8 @@ [displayCategories]="tenant()?.display_categories ?? true" [displaySeachBar]="tenant()?.display_seach_bar ?? true" [displayCart]="displayCart()" - (cartClick)="isCartOpen.set(!isCartOpen())" + [cartDisabled]="isCheckoutRoute()" + (cartClick)="onCartClick()" (ticketsClick)="onTicketsClick()" (loginClick)="onLoginClick()" (logoutClick)="onLogoutClick()" @@ -20,7 +21,7 @@ (categorySelect)="onCategorySelect($event)" /> - @if (displayCart() && isCartOpen()) { + @if (displayCart() && !isCheckoutRoute() && isCartOpen()) { { imports: [StoreLayoutComponent], providers: [ provideRouter([]), + { provide: UrlSerializer, useClass: TenantUrlSerializer }, { provide: ActivatedRoute, useValue: { queryParamMap: queryParamMapState.asObservable() }, @@ -240,6 +243,33 @@ describe('StoreLayoutComponent', () => { expect((fixture.nativeElement as HTMLElement).querySelector('app-cart')).not.toBeNull(); }); + it('disables the cart icon and prevents opening the popup during checkout with a tenant base path', () => { + tenantState.set({ ...tenant, base_path: 'fiesta' }); + const router = TestBed.inject(Router); + vi.spyOn(router, 'url', 'get').mockReturnValue('/fiesta/checkout?purchase=25'); + + const fixture = TestBed.createComponent(StoreLayoutComponent); + fixture.detectChanges(); + + const element = fixture.nativeElement as HTMLElement; + const cartButton = element.querySelector('app-cart-icon button'); + const header = fixture.debugElement.query(By.directive(StoreHeaderComponent)); + + expect(cartButton?.disabled).toBe(true); + expect(cartButton?.getAttribute('aria-label')).toBe( + 'Carrito no disponible durante el checkout', + ); + expect(element.querySelector('[data-testid="cart-badge"]')).toBeNull(); + + header.componentInstance.cartClick.emit(); + queryParamMapState.next(convertToParamMap({ openCart: 'true' })); + fixture.detectChanges(); + + expect((fixture.componentInstance as any).isCartOpen()).toBe(false); + expect(element.querySelector('app-cart')).toBeNull(); + expect(element.querySelector('.store-layout__cart-overlay')).toBeNull(); + }); + it('hides the configured header elements when the tenant disables them', () => { tenantState.set({ ...tenant, diff --git a/src/app/core/layout/store-layout/store-layout.component.ts b/src/app/core/layout/store-layout/store-layout.component.ts index b5740d0..53e8b10 100644 --- a/src/app/core/layout/store-layout/store-layout.component.ts +++ b/src/app/core/layout/store-layout/store-layout.component.ts @@ -1,6 +1,13 @@ import { Component, computed, DestroyRef, inject, OnInit, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { ActivatedRoute, Router, RouterOutlet } from '@angular/router'; +import { + ActivatedRoute, + NavigationEnd, + PRIMARY_OUTLET, + Router, + RouterOutlet, +} from '@angular/router'; +import { filter } from 'rxjs'; import { TenantService } from '../../services/tenant.service'; import { CartService } from '../../services/cart/cart.service'; import { StoreFooterComponent, StoreFooterSection } from './store-footer/store-footer.component'; @@ -37,6 +44,7 @@ export class StoreLayoutComponent implements OnInit { private readonly destroyRef = inject(DestroyRef); protected readonly isCartOpen = signal(false); + protected readonly isCheckoutRoute = signal(this.isCheckoutUrl(this.router.url)); protected readonly isCreatingPurchase = signal(false); protected readonly displayCart = computed(() => this.tenant()?.display_cart ?? true); protected readonly cartEditingPolicy = computed(() => this.tenant()?.cart_editing_policy ?? null); @@ -149,8 +157,22 @@ export class StoreLayoutComponent implements OnInit { }); ngOnInit(): void { + this.router.events + .pipe( + filter((event): event is NavigationEnd => event instanceof NavigationEnd), + takeUntilDestroyed(this.destroyRef), + ) + .subscribe((event) => { + const isCheckoutRoute = this.isCheckoutUrl(event.urlAfterRedirects); + this.isCheckoutRoute.set(isCheckoutRoute); + + if (isCheckoutRoute) { + this.isCartOpen.set(false); + } + }); + this.route.queryParamMap.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((params) => { - if (params.get('openCart') === 'true') { + if (params.get('openCart') === 'true' && !this.isCheckoutRoute()) { this.isCartOpen.set(true); } }); @@ -160,10 +182,24 @@ export class StoreLayoutComponent implements OnInit { }); } + private isCheckoutUrl(url: string): boolean { + const primarySegments = this.router.parseUrl(url).root.children[PRIMARY_OUTLET]?.segments ?? []; + + return primarySegments[0]?.path === 'checkout'; + } + protected onLoginClick(): void { void this.router.navigate(['/login']); } + protected onCartClick(): void { + if (this.isCheckoutRoute()) { + return; + } + + this.isCartOpen.update((isOpen) => !isOpen); + } + protected onSearch(term: string): void { void this.router.navigate(['/buscar'], { queryParams: { q: term, page: 1 }, diff --git a/src/app/shared/components/cart-icon/cart-icon.component.html b/src/app/shared/components/cart-icon/cart-icon.component.html index 7cf6e49..48cc7d4 100644 --- a/src/app/shared/components/cart-icon/cart-icon.component.html +++ b/src/app/shared/components/cart-icon/cart-icon.component.html @@ -1,7 +1,9 @@ diff --git a/src/app/shared/components/cart-icon/cart-icon.component.scss b/src/app/shared/components/cart-icon/cart-icon.component.scss index 49426b6..d9e3e7d 100644 --- a/src/app/shared/components/cart-icon/cart-icon.component.scss +++ b/src/app/shared/components/cart-icon/cart-icon.component.scss @@ -14,7 +14,9 @@ color: #666666; cursor: pointer; outline: none; - transition: color 0.15s ease-in-out, transform 0.1s ease-in-out; + transition: + color 0.15s ease-in-out, + transform 0.1s ease-in-out; border-radius: 4px; // Active state subtle scale down @@ -29,10 +31,12 @@ } // Disabled state - &:disabled { - color: #A0A0A0; + &:disabled, + &.cart-icon--disabled { + color: #b8b8b8; cursor: not-allowed; pointer-events: none; + opacity: 0.45; } } @@ -59,6 +63,11 @@ line-height: 1; } +.cart-icon:disabled .cart-icon__glyph, +.cart-icon--disabled .cart-icon__glyph { + color: #b8b8b8; +} + .cart-icon__badge { position: absolute; top: -8px; diff --git a/src/app/shared/components/cart-icon/cart-icon.component.spec.ts b/src/app/shared/components/cart-icon/cart-icon.component.spec.ts index eaca57a..0aecdac 100644 --- a/src/app/shared/components/cart-icon/cart-icon.component.spec.ts +++ b/src/app/shared/components/cart-icon/cart-icon.component.spec.ts @@ -6,7 +6,7 @@ import { CartIconComponent } from './cart-icon.component'; describe('CartIconComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [CartIconComponent] + imports: [CartIconComponent], }).compileComponents(); }); @@ -20,7 +20,7 @@ describe('CartIconComponent', () => { return { fixture, - element: fixture.nativeElement as HTMLElement + element: fixture.nativeElement as HTMLElement, }; } @@ -47,9 +47,11 @@ describe('CartIconComponent', () => { }); it('disables the button when disabled is true', () => { - const { element } = setup(undefined, true); + const { element } = setup(3, true); const button = element.querySelector('button'); expect(button?.disabled).toBe(true); + expect(button?.classList).toContain('cart-icon--disabled'); + expect(element.querySelector('[data-testid="cart-badge"]')).toBeNull(); }); it('enables the button when disabled is false', () => { diff --git a/src/app/shared/components/cart-icon/cart-icon.component.ts b/src/app/shared/components/cart-icon/cart-icon.component.ts index 25d59fc..75cd2d1 100644 --- a/src/app/shared/components/cart-icon/cart-icon.component.ts +++ b/src/app/shared/components/cart-icon/cart-icon.component.ts @@ -5,7 +5,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co imports: [], templateUrl: './cart-icon.component.html', styleUrl: './cart-icon.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class CartIconComponent { readonly quantity = input(undefined); @@ -13,6 +13,10 @@ export class CartIconComponent { readonly ariaLabel = input('Carrito de compras'); protected readonly hasQuantity = computed(() => { + if (this.disabled()) { + return false; + } + const q = this.quantity(); return q !== null && q !== undefined && q > 0; });