From 17dbf52181545d47128cff2d4e75e1c31faffcca Mon Sep 17 00:00:00 2001 From: ncoronel Date: Tue, 15 Sep 2026 17:06:24 -0300 Subject: [PATCH] feat(event): show date status information in storefront --- src/app/core/services/tenant.interface.ts | 4 +++ .../store-home-page.component.spec.ts | 6 ++++ .../store-home-page.component.ts | 2 ++ .../hero-banner/hero-banner.component.html | 7 +++- .../hero-banner/hero-banner.component.scss | 4 +++ .../hero-banner/hero-banner.component.spec.ts | 12 +++++-- .../hero-banner/hero-banner.component.ts | 3 +- .../components/tooltip/tooltip.component.html | 14 +++++++- .../components/tooltip/tooltip.component.scss | 23 ++++++------ .../tooltip/tooltip.component.spec.ts | 10 ++++++ .../components/tooltip/tooltip.component.ts | 36 +++++++++++++++++++ 11 files changed, 102 insertions(+), 19 deletions(-) diff --git a/src/app/core/services/tenant.interface.ts b/src/app/core/services/tenant.interface.ts index 8dbdfd6..6f8bff5 100644 --- a/src/app/core/services/tenant.interface.ts +++ b/src/app/core/services/tenant.interface.ts @@ -51,6 +51,8 @@ export interface EventDate { date: string; start_time: string; end_time: string; + info_text: string | null; + isCanceled: boolean; } export interface EventConfig { @@ -67,6 +69,8 @@ export interface ActiveEventDate { date: string; time_start: string; time_end: string; + info_text: string | null; + isCanceled: boolean; } export type TenantEventDateChangeType = 'rescheduled' | 'suspended'; diff --git a/src/app/features/store/pages/store-home-page/store-home-page.component.spec.ts b/src/app/features/store/pages/store-home-page/store-home-page.component.spec.ts index 60367d9..47e3c14 100644 --- a/src/app/features/store/pages/store-home-page/store-home-page.component.spec.ts +++ b/src/app/features/store/pages/store-home-page/store-home-page.component.spec.ts @@ -233,12 +233,16 @@ describe('StoreHomePageComponent', () => { date: '2026-12-05', time_start: '09:00:00', time_end: '18:00:00', + info_text: null, + isCanceled: false, }, { id: 21, date: '2026-12-06', time_start: '09:00:00', time_end: '18:00:00', + info_text: null, + isCanceled: false, }, ], }; @@ -369,6 +373,8 @@ describe('StoreHomePageComponent', () => { date: '2026-10-09', time_start: '09:00:00', time_end: '18:00:00', + info_text: null, + isCanceled: false, }, ], }; diff --git a/src/app/features/store/pages/store-home-page/store-home-page.component.ts b/src/app/features/store/pages/store-home-page/store-home-page.component.ts index 3092cd0..45e9c93 100644 --- a/src/app/features/store/pages/store-home-page/store-home-page.component.ts +++ b/src/app/features/store/pages/store-home-page/store-home-page.component.ts @@ -89,6 +89,8 @@ export class StoreHomePageComponent implements OnInit, OnDestroy { date: eventDate.date, start_time: eventDate.time_start, end_time: eventDate.time_end, + info_text: eventDate.info_text, + isCanceled: eventDate.isCanceled, })), contact, }; diff --git a/src/app/shared/components/hero-banner/hero-banner.component.html b/src/app/shared/components/hero-banner/hero-banner.component.html index 0fcf6db..c46dff9 100644 --- a/src/app/shared/components/hero-banner/hero-banner.component.html +++ b/src/app/shared/components/hero-banner/hero-banner.component.html @@ -88,7 +88,12 @@ - {{ formatScheduleDate(eventDate.date) }} + + {{ formatScheduleDate(eventDate.date) }} + + @if (eventDate.info_text) { + + } diff --git a/src/app/shared/components/hero-banner/hero-banner.component.scss b/src/app/shared/components/hero-banner/hero-banner.component.scss index 1b42b53..25223a1 100644 --- a/src/app/shared/components/hero-banner/hero-banner.component.scss +++ b/src/app/shared/components/hero-banner/hero-banner.component.scss @@ -164,6 +164,10 @@ } } +.event-schedule-date--canceled { + text-decoration: line-through; +} + @media (max-width: 767.98px) { .hero-banner-container { padding-bottom: 0; diff --git a/src/app/shared/components/hero-banner/hero-banner.component.spec.ts b/src/app/shared/components/hero-banner/hero-banner.component.spec.ts index 6d11bbe..8d7fd88 100644 --- a/src/app/shared/components/hero-banner/hero-banner.component.spec.ts +++ b/src/app/shared/components/hero-banner/hero-banner.component.spec.ts @@ -22,9 +22,7 @@ describe('HeroBannerComponent', () => { expect(element.querySelector('img')?.getAttribute('src')).toBe( 'https://example.com/desktop.jpg', ); - expect(element.querySelector('.hero-banner')?.classList).toContain( - 'hero-banner--with-media', - ); + expect(element.querySelector('.hero-banner')?.classList).toContain('hero-banner--with-media'); }); it('keeps the fallback banner sizing when there is no image', async () => { @@ -52,12 +50,16 @@ describe('HeroBannerComponent', () => { date: '2026-10-09', start_time: '10:00:00', end_time: '20:30:00', + info_text: 'Esta fecha fue cancelada.', + isCanceled: true, }, { id: 2, date: '2026-10-10', start_time: '10:00:00', end_time: '22:00:00', + info_text: null, + isCanceled: false, }, ], }); @@ -78,6 +80,10 @@ describe('HeroBannerComponent', () => { expect(element.querySelectorAll('.event-schedules tbody tr')).toHaveLength(2); expect(element.querySelector('.event-schedules')?.textContent).toContain('9 de Octubre 2026'); expect(element.querySelector('.event-schedules')?.textContent).toContain('10:00 - 20:30'); + expect(element.querySelector('.event-schedule-date--canceled')).not.toBeNull(); + expect(element.querySelector('app-tooltip')?.textContent).toContain( + 'Esta fecha fue cancelada.', + ); toggle?.click(); fixture.detectChanges(); diff --git a/src/app/shared/components/hero-banner/hero-banner.component.ts b/src/app/shared/components/hero-banner/hero-banner.component.ts index 4859c54..7509445 100644 --- a/src/app/shared/components/hero-banner/hero-banner.component.ts +++ b/src/app/shared/components/hero-banner/hero-banner.component.ts @@ -3,11 +3,12 @@ import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { EventConfig, HeroConfig } from '../../../core/services/tenant.interface'; import { ButtonComponent } from '../button/button.component'; +import { TooltipComponent } from '../tooltip/tooltip.component'; @Component({ selector: 'app-hero-banner', standalone: true, - imports: [CommonModule, RouterModule, ButtonComponent], + imports: [CommonModule, RouterModule, ButtonComponent, TooltipComponent], templateUrl: './hero-banner.component.html', styleUrl: './hero-banner.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/shared/components/tooltip/tooltip.component.html b/src/app/shared/components/tooltip/tooltip.component.html index 35788c8..f3c2475 100644 --- a/src/app/shared/components/tooltip/tooltip.component.html +++ b/src/app/shared/components/tooltip/tooltip.component.html @@ -3,9 +3,21 @@ class="tooltip-trigger" [attr.aria-describedby]="tooltipId" [attr.aria-label]="message()" + (mouseenter)="showTooltip($event)" + (mouseleave)="hideTooltip()" + (focus)="showTooltip($event)" + (blur)="hideTooltip()" > - + {{ message() }} diff --git a/src/app/shared/components/tooltip/tooltip.component.scss b/src/app/shared/components/tooltip/tooltip.component.scss index c195b68..e77d912 100644 --- a/src/app/shared/components/tooltip/tooltip.component.scss +++ b/src/app/shared/components/tooltip/tooltip.component.scss @@ -22,22 +22,15 @@ outline: 2px solid currentColor; outline-offset: 2px; } - - &:hover .tooltip-message, - &:focus-visible .tooltip-message { - visibility: visible; - opacity: 1; - transform: translate(-50%, -0.25rem); - } } .tooltip-message { - position: absolute; - z-index: 1100; - bottom: calc(100% + 0.625rem); - left: 50%; + position: fixed; + inset: auto; + z-index: 2000; width: max-content; max-width: min(16rem, 75vw); + margin: 0; padding: 0.5rem 0.75rem; border-radius: 0.375rem; border: 1px solid var(--tenant-primary, var(--color-primary, #0d6efd)); @@ -54,9 +47,13 @@ visibility: hidden; opacity: 0; pointer-events: none; - transform: translate(-50%, 0); + transform: translateX(-50%); transition: opacity 0.15s ease, - transform 0.15s ease, visibility 0.15s ease; } + +.tooltip-message--visible { + visibility: visible; + opacity: 1; +} diff --git a/src/app/shared/components/tooltip/tooltip.component.spec.ts b/src/app/shared/components/tooltip/tooltip.component.spec.ts index 1702ac7..c78b05a 100644 --- a/src/app/shared/components/tooltip/tooltip.component.spec.ts +++ b/src/app/shared/components/tooltip/tooltip.component.spec.ts @@ -28,5 +28,15 @@ describe('TooltipComponent', () => { expect(trigger?.querySelector('i.fa-solid.fa-circle-info')).not.toBeNull(); expect(message?.textContent?.trim()).toBe('Este producto no tiene stock disponible.'); expect(trigger?.getAttribute('aria-describedby')).toBe(message?.id); + + trigger?.dispatchEvent(new MouseEvent('mouseenter')); + fixture.detectChanges(); + + expect(message?.classList).toContain('tooltip-message--visible'); + + trigger?.dispatchEvent(new MouseEvent('mouseleave')); + fixture.detectChanges(); + + expect(message?.classList).not.toContain('tooltip-message--visible'); }); }); diff --git a/src/app/shared/components/tooltip/tooltip.component.ts b/src/app/shared/components/tooltip/tooltip.component.ts index 9f70c23..68786f8 100644 --- a/src/app/shared/components/tooltip/tooltip.component.ts +++ b/src/app/shared/components/tooltip/tooltip.component.ts @@ -11,4 +11,40 @@ let nextTooltipId = 0; export class TooltipComponent { readonly message = input.required(); protected readonly tooltipId = `app-tooltip-${nextTooltipId++}`; + protected tooltipVisible = false; + protected tooltipLeft = 0; + protected tooltipTop = 0; + private activeTooltip: HTMLElement | null = null; + + protected showTooltip(event: MouseEvent | FocusEvent): void { + const trigger = event.currentTarget as HTMLElement; + const rect = trigger.getBoundingClientRect(); + + this.tooltipLeft = rect.left + rect.width / 2; + this.tooltipTop = rect.bottom + 10; + this.tooltipVisible = true; + this.activeTooltip = trigger.querySelector('.tooltip-message'); + + if ( + this.activeTooltip && + typeof this.activeTooltip.showPopover === 'function' && + !this.activeTooltip.matches(':popover-open') + ) { + this.activeTooltip.showPopover(); + } + } + + protected hideTooltip(): void { + this.tooltipVisible = false; + + if ( + this.activeTooltip && + typeof this.activeTooltip.hidePopover === 'function' && + this.activeTooltip.matches(':popover-open') + ) { + this.activeTooltip.hidePopover(); + } + + this.activeTooltip = null; + } }