diff --git a/src/app/features/store/pages/account-page/pages/ticket-events-page/components/ticket-event-list-item/ticket-event-list-item.html b/src/app/features/store/pages/account-page/pages/ticket-events-page/components/ticket-event-list-item/ticket-event-list-item.html new file mode 100644 index 0000000..26d83df --- /dev/null +++ b/src/app/features/store/pages/account-page/pages/ticket-events-page/components/ticket-event-list-item/ticket-event-list-item.html @@ -0,0 +1,30 @@ + + + + @if (event().image; as image) { + + } + + + + {{ event().title }} + + + + + diff --git a/src/app/features/store/pages/account-page/pages/ticket-events-page/components/ticket-event-list-item/ticket-event-list-item.scss b/src/app/features/store/pages/account-page/pages/ticket-events-page/components/ticket-event-list-item/ticket-event-list-item.scss new file mode 100644 index 0000000..1114327 --- /dev/null +++ b/src/app/features/store/pages/account-page/pages/ticket-events-page/components/ticket-event-list-item/ticket-event-list-item.scss @@ -0,0 +1,60 @@ +:host { + display: block; + width: 100%; +} + +.ticket-event { + min-height: 84px; + padding: 12px 8px; + color: #666666; + text-decoration: none; + transition: + box-shadow 0.2s, + border-radius 0.2s; +} + +.ticket-event:hover, +.ticket-event:focus-visible { + border-radius: 8px; + box-shadow: 0 0 30px rgba(0, 0, 0, 0.07); +} + +.ticket-event:focus-visible { + outline: 2px solid var(--color-primary, var(--tenant-primary, #5b75ff)); + outline-offset: 2px; +} + +.ticket-event__image-wrapper { + display: block; + width: 100%; + aspect-ratio: 19 / 13; + overflow: hidden; + border-radius: 3px; + background: #dddddd; +} + +.ticket-event__image { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +.ticket-event__title { + overflow: hidden; + font-size: 13px; + font-weight: 700; + line-height: 1.3; + text-overflow: ellipsis; + text-transform: uppercase; +} + +.ticket-event__arrow { + color: #888888; + transition: color 0.2s; +} + +.ticket-event:hover .ticket-event__arrow, +.ticket-event:focus-visible .ticket-event__arrow { + color: var(--color-primary, var(--tenant-primary, #5b75ff)); +} diff --git a/src/app/features/store/pages/account-page/pages/ticket-events-page/components/ticket-event-list-item/ticket-event-list-item.ts b/src/app/features/store/pages/account-page/pages/ticket-events-page/components/ticket-event-list-item/ticket-event-list-item.ts new file mode 100644 index 0000000..1796b2f --- /dev/null +++ b/src/app/features/store/pages/account-page/pages/ticket-events-page/components/ticket-event-list-item/ticket-event-list-item.ts @@ -0,0 +1,14 @@ +import { Component, input } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +import { TicketEventResponse } from '../../../tickets-page/ticket.service'; + +@Component({ + selector: 'app-ticket-event-list-item', + imports: [RouterLink], + templateUrl: './ticket-event-list-item.html', + styleUrl: './ticket-event-list-item.scss', +}) +export class TicketEventListItem { + readonly event = input.required(); +} diff --git a/src/app/features/store/pages/account-page/pages/ticket-events-page/ticket-events-page.html b/src/app/features/store/pages/account-page/pages/ticket-events-page/ticket-events-page.html new file mode 100644 index 0000000..6934ace --- /dev/null +++ b/src/app/features/store/pages/account-page/pages/ticket-events-page/ticket-events-page.html @@ -0,0 +1,35 @@ + +
+ @if (isLoading()) { + @for (item of [1, 2, 3, 4]; track item) { + + } + } @else if (events().length) { + @for (event of events(); track event.id; let last = $last) { + + @if (!last) { +
+ } + } + } @else { +

Aún no tenés eventos con tickets.

+ } +
+
diff --git a/src/app/features/store/pages/account-page/pages/ticket-events-page/ticket-events-page.scss b/src/app/features/store/pages/account-page/pages/ticket-events-page/ticket-events-page.scss new file mode 100644 index 0000000..4c0d23d --- /dev/null +++ b/src/app/features/store/pages/account-page/pages/ticket-events-page/ticket-events-page.scss @@ -0,0 +1,66 @@ +:host { + display: block; + width: 100%; +} + +.ticket-events-page { + display: block; + width: 100%; + max-width: 600px; +} + +.ticket-events-list { + display: flex; + flex-direction: column; + width: 100%; +} + +.ticket-events-list__divider { + width: 100%; + height: 1px; + background: var(--border-color, #dddddd); +} + +.ticket-events-list__empty { + margin: 0; + padding: 2rem 0; + color: #666666; + font-size: 1rem; + text-align: center; +} + +.ticket-events-list__skeleton { + min-height: 84px; + padding: 12px 8px; + border-bottom: 1px solid var(--border-color, #dddddd); +} + +.ticket-events-list__skeleton-image, +.ticket-events-list__skeleton-title, +.ticket-events-list__skeleton-arrow { + display: block; + border-radius: 4px; + background: #eeeeee; + animation: ticket-events-pulse 1.5s infinite ease-in-out; +} + +.ticket-events-list__skeleton-image { + width: 100%; + aspect-ratio: 19 / 13; +} + +.ticket-events-list__skeleton-title { + width: min(250px, 80%); + height: 14px; +} + +.ticket-events-list__skeleton-arrow { + width: 8px; + height: 14px; +} + +@keyframes ticket-events-pulse { + 50% { + background-color: #e0e0e0; + } +} diff --git a/src/app/features/store/pages/account-page/pages/ticket-events-page/ticket-events-page.spec.ts b/src/app/features/store/pages/account-page/pages/ticket-events-page/ticket-events-page.spec.ts new file mode 100644 index 0000000..69d5ec8 --- /dev/null +++ b/src/app/features/store/pages/account-page/pages/ticket-events-page/ticket-events-page.spec.ts @@ -0,0 +1,99 @@ +import { signal } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { TenantService } from '../../../../../../core/services/tenant.service'; +import { ToastService } from '../../../../../../core/services/toast.service'; +import { TicketService } from '../tickets-page/ticket.service'; +import { TicketEventsPage } from './ticket-events-page'; + +function withCustomLoading(service: T): T & { withCustomLoading: () => T } { + return Object.assign(service, { withCustomLoading: () => service }); +} + +describe('TicketEventsPage', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideRouter([]), + { + provide: TenantService, + useValue: { + tenant: signal({ + menues: [ + { + id: 4, + code: 'account.tickets', + label: 'Tickets', + parent_menu_code: 'account', + content_type: 'dynamic', + route: '/mi-cuenta/tickets', + submenues: [], + }, + ], + }).asReadonly(), + }, + }, + { provide: ToastService, useValue: { danger: vi.fn() } }, + ], + }); + }); + + it('shows the users events with image, title and detail link', async () => { + await TestBed.configureTestingModule({ imports: [TicketEventsPage] }) + .overrideComponent(TicketEventsPage, { + set: { + providers: [ + { + provide: TicketService, + useValue: withCustomLoading({ + getTicketEvents: () => + Promise.resolve([ + { id: 27, title: 'Copa del mundo', image: 'https://example.com/event.jpg' }, + ]), + }), + }, + ], + }, + }) + .compileComponents(); + + const fixture = TestBed.createComponent(TicketEventsPage); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + const page = fixture.nativeElement as HTMLElement; + const link = page.querySelector('.ticket-event'); + + expect(page.textContent).toContain('TICKETS'); + expect(page.textContent).toContain('Copa del mundo'); + expect(link?.getAttribute('href')).toBe('/27'); + expect(link?.querySelector('img')?.getAttribute('src')).toBe('https://example.com/event.jpg'); + }); + + it('shows the empty state when the user has no ticket events', async () => { + await TestBed.configureTestingModule({ imports: [TicketEventsPage] }) + .overrideComponent(TicketEventsPage, { + set: { + providers: [ + { + provide: TicketService, + useValue: withCustomLoading({ getTicketEvents: () => Promise.resolve([]) }), + }, + ], + }, + }) + .compileComponents(); + + const fixture = TestBed.createComponent(TicketEventsPage); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + expect((fixture.nativeElement as HTMLElement).textContent).toContain( + 'Aún no tenés eventos con tickets.', + ); + }); +}); diff --git a/src/app/features/store/pages/account-page/pages/ticket-events-page/ticket-events-page.ts b/src/app/features/store/pages/account-page/pages/ticket-events-page/ticket-events-page.ts new file mode 100644 index 0000000..186b19c --- /dev/null +++ b/src/app/features/store/pages/account-page/pages/ticket-events-page/ticket-events-page.ts @@ -0,0 +1,31 @@ +import { Component, inject, OnInit, signal } from '@angular/core'; + +import { ToastService } from '../../../../../../core/services/toast.service'; +import { MenuContentSectionComponent } from '../../../../components/menu-content-section/menu-content-section.component'; +import { TicketEventResponse, TicketService } from '../tickets-page/ticket.service'; +import { TicketEventListItem } from './components/ticket-event-list-item/ticket-event-list-item'; + +@Component({ + selector: 'app-ticket-events-page', + imports: [MenuContentSectionComponent, TicketEventListItem], + providers: [TicketService], + templateUrl: './ticket-events-page.html', + styleUrl: './ticket-events-page.scss', +}) +export class TicketEventsPage implements OnInit { + private readonly ticketService = inject(TicketService); + private readonly toastService = inject(ToastService); + + protected readonly events = signal([]); + protected readonly isLoading = signal(true); + + async ngOnInit(): Promise { + try { + this.events.set(await this.ticketService.withCustomLoading().getTicketEvents()); + } catch { + this.toastService.danger('Hubo un error al cargar los eventos'); + } finally { + this.isLoading.set(false); + } + } +} diff --git a/src/app/features/store/store.routes.ts b/src/app/features/store/store.routes.ts index 9d1bd50..ff49c0c 100644 --- a/src/app/features/store/store.routes.ts +++ b/src/app/features/store/store.routes.ts @@ -231,6 +231,24 @@ export const routes: Routes = [ (m) => m.PurchaseDetailPage, ), }, + { + path: 'tickets/:eventId', + canMatch: [isMultiEventStorefront], + canActivate: [hasMenuGuard('account.tickets')], + loadComponent: () => + import('./pages/account-page/pages/tickets-page/tickets-page').then( + (m) => m.TicketsPage, + ), + }, + { + path: 'tickets', + canMatch: [isMultiEventStorefront], + canActivate: [hasMenuGuard('account.tickets')], + loadComponent: () => + import('./pages/account-page/pages/ticket-events-page/ticket-events-page').then( + (m) => m.TicketEventsPage, + ), + }, { path: 'tickets', canActivate: [hasMenuGuard('account.tickets')],