feat(event): show date status information in storefront

This commit is contained in:
ncoronel 2026-09-15 17:06:24 -03:00
parent a7e1d8995f
commit 17dbf52181
11 changed files with 102 additions and 19 deletions

View File

@ -51,6 +51,8 @@ export interface EventDate {
date: string; date: string;
start_time: string; start_time: string;
end_time: string; end_time: string;
info_text: string | null;
isCanceled: boolean;
} }
export interface EventConfig { export interface EventConfig {
@ -67,6 +69,8 @@ export interface ActiveEventDate {
date: string; date: string;
time_start: string; time_start: string;
time_end: string; time_end: string;
info_text: string | null;
isCanceled: boolean;
} }
export type TenantEventDateChangeType = 'rescheduled' | 'suspended'; export type TenantEventDateChangeType = 'rescheduled' | 'suspended';

View File

@ -233,12 +233,16 @@ describe('StoreHomePageComponent', () => {
date: '2026-12-05', date: '2026-12-05',
time_start: '09:00:00', time_start: '09:00:00',
time_end: '18:00:00', time_end: '18:00:00',
info_text: null,
isCanceled: false,
}, },
{ {
id: 21, id: 21,
date: '2026-12-06', date: '2026-12-06',
time_start: '09:00:00', time_start: '09:00:00',
time_end: '18:00:00', time_end: '18:00:00',
info_text: null,
isCanceled: false,
}, },
], ],
}; };
@ -369,6 +373,8 @@ describe('StoreHomePageComponent', () => {
date: '2026-10-09', date: '2026-10-09',
time_start: '09:00:00', time_start: '09:00:00',
time_end: '18:00:00', time_end: '18:00:00',
info_text: null,
isCanceled: false,
}, },
], ],
}; };

View File

@ -89,6 +89,8 @@ export class StoreHomePageComponent implements OnInit, OnDestroy {
date: eventDate.date, date: eventDate.date,
start_time: eventDate.time_start, start_time: eventDate.time_start,
end_time: eventDate.time_end, end_time: eventDate.time_end,
info_text: eventDate.info_text,
isCanceled: eventDate.isCanceled,
})), })),
contact, contact,
}; };

View File

@ -88,7 +88,12 @@
<td> <td>
<span class="event-schedule-value"> <span class="event-schedule-value">
<i class="fa-regular fa-calendar" aria-hidden="true"></i> <i class="fa-regular fa-calendar" aria-hidden="true"></i>
<span>{{ formatScheduleDate(eventDate.date) }}</span> <span [class.event-schedule-date--canceled]="eventDate.isCanceled">
{{ formatScheduleDate(eventDate.date) }}
</span>
@if (eventDate.info_text) {
<app-tooltip [message]="eventDate.info_text" />
}
</span> </span>
</td> </td>
<td> <td>

View File

@ -164,6 +164,10 @@
} }
} }
.event-schedule-date--canceled {
text-decoration: line-through;
}
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
.hero-banner-container { .hero-banner-container {
padding-bottom: 0; padding-bottom: 0;

View File

@ -22,9 +22,7 @@ describe('HeroBannerComponent', () => {
expect(element.querySelector('img')?.getAttribute('src')).toBe( expect(element.querySelector('img')?.getAttribute('src')).toBe(
'https://example.com/desktop.jpg', 'https://example.com/desktop.jpg',
); );
expect(element.querySelector('.hero-banner')?.classList).toContain( expect(element.querySelector('.hero-banner')?.classList).toContain('hero-banner--with-media');
'hero-banner--with-media',
);
}); });
it('keeps the fallback banner sizing when there is no image', async () => { it('keeps the fallback banner sizing when there is no image', async () => {
@ -52,12 +50,16 @@ describe('HeroBannerComponent', () => {
date: '2026-10-09', date: '2026-10-09',
start_time: '10:00:00', start_time: '10:00:00',
end_time: '20:30:00', end_time: '20:30:00',
info_text: 'Esta fecha fue cancelada.',
isCanceled: true,
}, },
{ {
id: 2, id: 2,
date: '2026-10-10', date: '2026-10-10',
start_time: '10:00:00', start_time: '10:00:00',
end_time: '22: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.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('9 de Octubre 2026');
expect(element.querySelector('.event-schedules')?.textContent).toContain('10:00 - 20:30'); 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(); toggle?.click();
fixture.detectChanges(); fixture.detectChanges();

View File

@ -3,11 +3,12 @@ import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { EventConfig, HeroConfig } from '../../../core/services/tenant.interface'; import { EventConfig, HeroConfig } from '../../../core/services/tenant.interface';
import { ButtonComponent } from '../button/button.component'; import { ButtonComponent } from '../button/button.component';
import { TooltipComponent } from '../tooltip/tooltip.component';
@Component({ @Component({
selector: 'app-hero-banner', selector: 'app-hero-banner',
standalone: true, standalone: true,
imports: [CommonModule, RouterModule, ButtonComponent], imports: [CommonModule, RouterModule, ButtonComponent, TooltipComponent],
templateUrl: './hero-banner.component.html', templateUrl: './hero-banner.component.html',
styleUrl: './hero-banner.component.scss', styleUrl: './hero-banner.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,

View File

@ -3,9 +3,21 @@
class="tooltip-trigger" class="tooltip-trigger"
[attr.aria-describedby]="tooltipId" [attr.aria-describedby]="tooltipId"
[attr.aria-label]="message()" [attr.aria-label]="message()"
(mouseenter)="showTooltip($event)"
(mouseleave)="hideTooltip()"
(focus)="showTooltip($event)"
(blur)="hideTooltip()"
> >
<i class="fa-solid fa-circle-info" aria-hidden="true"></i> <i class="fa-solid fa-circle-info" aria-hidden="true"></i>
<span class="tooltip-message" [id]="tooltipId" role="tooltip"> <span
class="tooltip-message"
[class.tooltip-message--visible]="tooltipVisible"
[style.left.px]="tooltipLeft"
[style.top.px]="tooltipTop"
[id]="tooltipId"
popover="manual"
role="tooltip"
>
{{ message() }} {{ message() }}
</span> </span>
</button> </button>

View File

@ -22,22 +22,15 @@
outline: 2px solid currentColor; outline: 2px solid currentColor;
outline-offset: 2px; outline-offset: 2px;
} }
&:hover .tooltip-message,
&:focus-visible .tooltip-message {
visibility: visible;
opacity: 1;
transform: translate(-50%, -0.25rem);
}
} }
.tooltip-message { .tooltip-message {
position: absolute; position: fixed;
z-index: 1100; inset: auto;
bottom: calc(100% + 0.625rem); z-index: 2000;
left: 50%;
width: max-content; width: max-content;
max-width: min(16rem, 75vw); max-width: min(16rem, 75vw);
margin: 0;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
border-radius: 0.375rem; border-radius: 0.375rem;
border: 1px solid var(--tenant-primary, var(--color-primary, #0d6efd)); border: 1px solid var(--tenant-primary, var(--color-primary, #0d6efd));
@ -54,9 +47,13 @@
visibility: hidden; visibility: hidden;
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
transform: translate(-50%, 0); transform: translateX(-50%);
transition: transition:
opacity 0.15s ease, opacity 0.15s ease,
transform 0.15s ease,
visibility 0.15s ease; visibility 0.15s ease;
} }
.tooltip-message--visible {
visibility: visible;
opacity: 1;
}

View File

@ -28,5 +28,15 @@ describe('TooltipComponent', () => {
expect(trigger?.querySelector('i.fa-solid.fa-circle-info')).not.toBeNull(); expect(trigger?.querySelector('i.fa-solid.fa-circle-info')).not.toBeNull();
expect(message?.textContent?.trim()).toBe('Este producto no tiene stock disponible.'); expect(message?.textContent?.trim()).toBe('Este producto no tiene stock disponible.');
expect(trigger?.getAttribute('aria-describedby')).toBe(message?.id); 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');
}); });
}); });

View File

@ -11,4 +11,40 @@ let nextTooltipId = 0;
export class TooltipComponent { export class TooltipComponent {
readonly message = input.required<string>(); readonly message = input.required<string>();
protected readonly tooltipId = `app-tooltip-${nextTooltipId++}`; 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<HTMLElement>('.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;
}
} }