feat(store-header): display checkout countdown
This commit is contained in:
@@ -153,7 +153,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="store-layout__categories d-none d-md-block">
|
<div class="store-layout__categories d-none d-md-block">
|
||||||
<div class="container-xl px-3 px-md-4">
|
<div class="container-xl h-100 px-3 px-md-4 d-flex align-items-center">
|
||||||
@if (displayCategories()) {
|
@if (displayCategories()) {
|
||||||
<div class="store-layout__category-menu">
|
<div class="store-layout__category-menu">
|
||||||
<button
|
<button
|
||||||
@@ -176,6 +176,17 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (checkoutRemainingTime(); as remainingTime) {
|
||||||
|
<div
|
||||||
|
class="store-layout__checkout-timer ms-auto d-flex align-items-baseline gap-3"
|
||||||
|
role="timer"
|
||||||
|
aria-label="Tiempo restante de compra"
|
||||||
|
>
|
||||||
|
<span class="store-layout__checkout-timer-label">Tiempo restante de compra:</span>
|
||||||
|
<span class="store-layout__checkout-timer-value">{{ remainingTime }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -14,6 +14,22 @@
|
|||||||
height: 3.5rem;
|
height: 3.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.store-layout__checkout-timer {
|
||||||
|
color: var(--tenant-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-layout__checkout-timer-label {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-layout__checkout-timer-value {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
.store-layout__brand-slot {
|
.store-layout__brand-slot {
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
import { Component, ElementRef, HostListener, inject, input, output, signal } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
computed,
|
||||||
|
ElementRef,
|
||||||
|
HostListener,
|
||||||
|
inject,
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
signal,
|
||||||
|
} from '@angular/core';
|
||||||
import { Router, RouterLink } from '@angular/router';
|
import { Router, RouterLink } from '@angular/router';
|
||||||
import { AuthUser } from '../../../services/auth/auth.interfaces';
|
import { AuthUser } from '../../../services/auth/auth.interfaces';
|
||||||
import { CartIconComponent } from '../../../../shared/components/cart-icon/cart-icon.component';
|
import { CartIconComponent } from '../../../../shared/components/cart-icon/cart-icon.component';
|
||||||
@@ -40,6 +49,7 @@ export class StoreHeaderComponent {
|
|||||||
readonly displaySeachBar = input(true);
|
readonly displaySeachBar = input(true);
|
||||||
readonly displayCart = input(true);
|
readonly displayCart = input(true);
|
||||||
readonly cartDisabled = input(false);
|
readonly cartDisabled = input(false);
|
||||||
|
readonly checkoutRemainingSeconds = input<number | null>(null);
|
||||||
readonly cartClick = output<void>();
|
readonly cartClick = output<void>();
|
||||||
readonly ticketsClick = output<void>();
|
readonly ticketsClick = output<void>();
|
||||||
readonly loginClick = output<void>();
|
readonly loginClick = output<void>();
|
||||||
@@ -53,6 +63,18 @@ export class StoreHeaderComponent {
|
|||||||
protected readonly minSearchLength = 3;
|
protected readonly minSearchLength = 3;
|
||||||
protected readonly showSearchError = signal(false);
|
protected readonly showSearchError = signal(false);
|
||||||
protected readonly searchControl = new FormControl('', { nonNullable: true });
|
protected readonly searchControl = new FormControl('', { nonNullable: true });
|
||||||
|
protected readonly checkoutRemainingTime = computed(() => {
|
||||||
|
const remainingSeconds = this.checkoutRemainingSeconds();
|
||||||
|
|
||||||
|
if (remainingSeconds === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const minutes = Math.floor(remainingSeconds / 60);
|
||||||
|
const seconds = remainingSeconds % 60;
|
||||||
|
|
||||||
|
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||||
|
});
|
||||||
|
|
||||||
protected onCartClick(): void {
|
protected onCartClick(): void {
|
||||||
if (this.cartDisabled()) {
|
if (this.cartDisabled()) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
[displaySeachBar]="tenant()?.display_seach_bar ?? true"
|
[displaySeachBar]="tenant()?.display_seach_bar ?? true"
|
||||||
[displayCart]="displayCart()"
|
[displayCart]="displayCart()"
|
||||||
[cartDisabled]="isCheckoutRoute()"
|
[cartDisabled]="isCheckoutRoute()"
|
||||||
|
[checkoutRemainingSeconds]="isCheckoutRoute() ? checkoutRemainingSeconds() : null"
|
||||||
(cartClick)="onCartClick()"
|
(cartClick)="onCartClick()"
|
||||||
(ticketsClick)="onTicketsClick()"
|
(ticketsClick)="onTicketsClick()"
|
||||||
(loginClick)="onLoginClick()"
|
(loginClick)="onLoginClick()"
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { AuthUser } from '../../services/auth/auth.interfaces';
|
|||||||
import { StoreLayoutComponent } from './store-layout.component';
|
import { StoreLayoutComponent } from './store-layout.component';
|
||||||
import { StoreHeaderComponent } from './store-header/store-header.component';
|
import { StoreHeaderComponent } from './store-header/store-header.component';
|
||||||
import { CheckoutService } from '../../services/checkout.service';
|
import { CheckoutService } from '../../services/checkout.service';
|
||||||
|
import { CheckoutCountdownService } from '../../services/checkout-countdown.service';
|
||||||
import { TenantUrlSerializer } from '../../services/tenant-url.serializer';
|
import { TenantUrlSerializer } from '../../services/tenant-url.serializer';
|
||||||
|
|
||||||
const tenant: Tenant = {
|
const tenant: Tenant = {
|
||||||
@@ -150,6 +151,7 @@ describe('StoreLayoutComponent', () => {
|
|||||||
let tenantState = signal<Tenant | null>(tenant);
|
let tenantState = signal<Tenant | null>(tenant);
|
||||||
let cartState = signal<Cart | null>(null);
|
let cartState = signal<Cart | null>(null);
|
||||||
let authUserState = signal<AuthUser | null>(null);
|
let authUserState = signal<AuthUser | null>(null);
|
||||||
|
let checkoutRemainingSecondsState = signal<number | null>(null);
|
||||||
let checkoutServiceStub: { startCheckout: ReturnType<typeof vi.fn> };
|
let checkoutServiceStub: { startCheckout: ReturnType<typeof vi.fn> };
|
||||||
let queryParamMapState: BehaviorSubject<ParamMap>;
|
let queryParamMapState: BehaviorSubject<ParamMap>;
|
||||||
|
|
||||||
@@ -157,6 +159,7 @@ describe('StoreLayoutComponent', () => {
|
|||||||
tenantState = signal<Tenant | null>(tenant);
|
tenantState = signal<Tenant | null>(tenant);
|
||||||
cartState = signal<Cart | null>(null);
|
cartState = signal<Cart | null>(null);
|
||||||
authUserState = signal<AuthUser | null>(null);
|
authUserState = signal<AuthUser | null>(null);
|
||||||
|
checkoutRemainingSecondsState = signal<number | null>(null);
|
||||||
queryParamMapState = new BehaviorSubject(convertToParamMap({}));
|
queryParamMapState = new BehaviorSubject(convertToParamMap({}));
|
||||||
const isAuthenticatedState = signal(false);
|
const isAuthenticatedState = signal(false);
|
||||||
checkoutServiceStub = {
|
checkoutServiceStub = {
|
||||||
@@ -215,6 +218,12 @@ describe('StoreLayoutComponent', () => {
|
|||||||
provide: CheckoutService,
|
provide: CheckoutService,
|
||||||
useValue: checkoutServiceStub,
|
useValue: checkoutServiceStub,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: CheckoutCountdownService,
|
||||||
|
useValue: {
|
||||||
|
remainingSeconds: checkoutRemainingSecondsState.asReadonly(),
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
@@ -271,6 +280,24 @@ describe('StoreLayoutComponent', () => {
|
|||||||
expect(element.querySelector('.store-layout__cart-overlay')).toBeNull();
|
expect(element.querySelector('.store-layout__cart-overlay')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows the synchronized checkout countdown aligned in the header', () => {
|
||||||
|
const router = TestBed.inject(Router);
|
||||||
|
vi.spyOn(router, 'url', 'get').mockReturnValue('/checkout/25');
|
||||||
|
checkoutRemainingSecondsState.set(587);
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(StoreLayoutComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const element = fixture.nativeElement as HTMLElement;
|
||||||
|
|
||||||
|
expect(element.querySelector('.store-layout__checkout-timer-label')?.textContent).toContain(
|
||||||
|
'Tiempo restante de compra:',
|
||||||
|
);
|
||||||
|
expect(element.querySelector('.store-layout__checkout-timer-value')?.textContent?.trim()).toBe(
|
||||||
|
'09:47',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('hides the configured header elements when the tenant disables them', () => {
|
it('hides the configured header elements when the tenant disables them', () => {
|
||||||
tenantState.set({
|
tenantState.set({
|
||||||
...tenant,
|
...tenant,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
} from '../../services/checkout.service';
|
} from '../../services/checkout.service';
|
||||||
import { ToastService } from '../../services/toast.service';
|
import { ToastService } from '../../services/toast.service';
|
||||||
import { Category } from '../../services/tenant.interface';
|
import { Category } from '../../services/tenant.interface';
|
||||||
|
import { CheckoutCountdownService } from '../../services/checkout-countdown.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-store-layout',
|
selector: 'app-store-layout',
|
||||||
@@ -42,6 +43,7 @@ export class StoreLayoutComponent implements OnInit {
|
|||||||
private readonly cartService = inject(CartService);
|
private readonly cartService = inject(CartService);
|
||||||
private readonly authService = inject(AuthService);
|
private readonly authService = inject(AuthService);
|
||||||
private readonly checkoutService = inject(CheckoutService);
|
private readonly checkoutService = inject(CheckoutService);
|
||||||
|
private readonly checkoutCountdownService = inject(CheckoutCountdownService);
|
||||||
private readonly toastService = inject(ToastService);
|
private readonly toastService = inject(ToastService);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
private readonly route = inject(ActivatedRoute);
|
private readonly route = inject(ActivatedRoute);
|
||||||
@@ -49,6 +51,7 @@ export class StoreLayoutComponent implements OnInit {
|
|||||||
|
|
||||||
protected readonly isCartOpen = signal(false);
|
protected readonly isCartOpen = signal(false);
|
||||||
protected readonly isCheckoutRoute = signal(this.isCheckoutUrl(this.router.url));
|
protected readonly isCheckoutRoute = signal(this.isCheckoutUrl(this.router.url));
|
||||||
|
protected readonly checkoutRemainingSeconds = this.checkoutCountdownService.remainingSeconds;
|
||||||
protected readonly isCreatingPurchase = signal(false);
|
protected readonly isCreatingPurchase = signal(false);
|
||||||
protected readonly displayCart = computed(() => this.tenant()?.display_cart ?? true);
|
protected readonly displayCart = computed(() => this.tenant()?.display_cart ?? true);
|
||||||
protected readonly cartEditingPolicy = computed(() => this.tenant()?.cart_editing_policy ?? null);
|
protected readonly cartEditingPolicy = computed(() => this.tenant()?.cart_editing_policy ?? null);
|
||||||
|
|||||||
Reference in New Issue
Block a user