feat(event): show date status information in storefront
This commit is contained in:
parent
a7e1d8995f
commit
17dbf52181
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue