From 6997550767efccb07bc6c92380014907edc858b1 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Fri, 18 Sep 2026 14:33:49 -0300 Subject: [PATCH] feat(events): add multi-event storefront pages --- .../event-card/event-card.component.html | 19 ++++++ .../event-card/event-card.component.scss | 11 ++++ .../event-card/event-card.component.ts | 15 +++++ .../event-list/event-list.component.html | 20 ++++++ .../event-list/event-list.component.scss | 5 ++ .../event-list/event-list.component.ts | 66 +++++++++++++++++++ .../event-detail-page.component.html | 19 ++++++ .../event-detail-page.component.scss | 8 +++ .../event-detail-page.component.ts | 35 ++++++++++ .../event-home-page.component.ts | 10 +++ .../event-search-page.component.ts | 10 +++ src/app/features/store/store.routes.ts | 27 +++++++- 12 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 src/app/features/store/components/event-card/event-card.component.html create mode 100644 src/app/features/store/components/event-card/event-card.component.scss create mode 100644 src/app/features/store/components/event-card/event-card.component.ts create mode 100644 src/app/features/store/components/event-list/event-list.component.html create mode 100644 src/app/features/store/components/event-list/event-list.component.scss create mode 100644 src/app/features/store/components/event-list/event-list.component.ts create mode 100644 src/app/features/store/pages/event-detail-page/event-detail-page.component.html create mode 100644 src/app/features/store/pages/event-detail-page/event-detail-page.component.scss create mode 100644 src/app/features/store/pages/event-detail-page/event-detail-page.component.ts create mode 100644 src/app/features/store/pages/event-home-page/event-home-page.component.ts create mode 100644 src/app/features/store/pages/event-search-page/event-search-page.component.ts diff --git a/src/app/features/store/components/event-card/event-card.component.html b/src/app/features/store/components/event-card/event-card.component.html new file mode 100644 index 0000000..0031deb --- /dev/null +++ b/src/app/features/store/components/event-card/event-card.component.html @@ -0,0 +1,19 @@ + +
+ @if (event().image) { + + } +
+
+

{{ event().title }}

+ @if (event().subtitle) { +

{{ event().subtitle }}

+ } + @if (event().date_text) { +

{{ event().date_text }}

+ } + @if (event().location) { +

{{ event().location }}

+ } +
+
diff --git a/src/app/features/store/components/event-card/event-card.component.scss b/src/app/features/store/components/event-card/event-card.component.scss new file mode 100644 index 0000000..6e4491d --- /dev/null +++ b/src/app/features/store/components/event-card/event-card.component.scss @@ -0,0 +1,11 @@ +:host { display: block; min-width: 0; } +.event-card { display: block; height: 100%; color: inherit; background: #fff; text-decoration: none; box-shadow: 0 12px 30px rgba(0, 0, 0, .05); transition: transform .2s, box-shadow .2s; } +.event-card:hover { transform: translateY(-3px); box-shadow: 0 16px 32px rgba(0, 0, 0, .1); } +.event-card:focus-visible { outline: 2px solid var(--tenant-primary); outline-offset: 3px; } +.event-card__image { width: 100%; aspect-ratio: 1.43; background: #e5e6e7; } +.event-card__image img { width: 100%; height: 100%; object-fit: cover; display: block; } +.event-card__body { padding: 1rem 1.1rem 1.2rem; } +.event-card__title { margin: 0; font-size: .9rem; font-weight: 700; line-height: 1.3; text-transform: uppercase; } +.event-card__subtitle { margin: .25rem 0 .8rem; font-size: .75rem; color: #63666a; } +.event-card__meta { display: flex; align-items: baseline; gap: .45rem; margin: .2rem 0 0; font-size: .7rem; color: #72767a; } +.event-card__meta i { width: .75rem; flex: none; } diff --git a/src/app/features/store/components/event-card/event-card.component.ts b/src/app/features/store/components/event-card/event-card.component.ts new file mode 100644 index 0000000..b61da65 --- /dev/null +++ b/src/app/features/store/components/event-card/event-card.component.ts @@ -0,0 +1,15 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { RouterLink } from '@angular/router'; + +import { StoreEvent } from '../../../../core/services/event/event.interface'; + +@Component({ + selector: 'app-event-card', + imports: [RouterLink], + templateUrl: './event-card.component.html', + styleUrl: './event-card.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class EventCardComponent { + readonly event = input.required(); +} diff --git a/src/app/features/store/components/event-list/event-list.component.html b/src/app/features/store/components/event-list/event-list.component.html new file mode 100644 index 0000000..ec4f385 --- /dev/null +++ b/src/app/features/store/components/event-list/event-list.component.html @@ -0,0 +1,20 @@ +@if (loading()) { +

Cargando eventos...

+} @else if (error()) { + +} @else if (results(); as page) { + @if (page.data.length) { +
+ @for (event of page.data; track event.id) { + + } +
+ @if (page.meta.last_page > 1) { +
+ +
+ } + } @else { +

{{ search() ? 'No encontramos eventos para tu búsqueda.' : 'No hay eventos disponibles en este momento.' }}

+ } +} diff --git a/src/app/features/store/components/event-list/event-list.component.scss b/src/app/features/store/components/event-list/event-list.component.scss new file mode 100644 index 0000000..8fd1cf0 --- /dev/null +++ b/src/app/features/store/components/event-list/event-list.component.scss @@ -0,0 +1,5 @@ +.event-list__grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: clamp(1.25rem, 3vw, 2.5rem); } +.event-list__pagination { display: flex; justify-content: center; margin-top: 3rem; } +.event-list__message { text-align: center; padding: 2rem 0; } +@media (max-width: 767.98px) { .event-list__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +@media (max-width: 479.98px) { .event-list__grid { grid-template-columns: 1fr; } } diff --git a/src/app/features/store/components/event-list/event-list.component.ts b/src/app/features/store/components/event-list/event-list.component.ts new file mode 100644 index 0000000..e533c8d --- /dev/null +++ b/src/app/features/store/components/event-list/event-list.component.ts @@ -0,0 +1,66 @@ +import { ChangeDetectionStrategy, Component, DestroyRef, inject, input, OnInit, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ActivatedRoute, Router } from '@angular/router'; +import { catchError, distinctUntilChanged, map, of, switchMap, tap } from 'rxjs'; + +import { EventService } from '../../../../core/services/event/event.service'; +import { StoreEventPage } from '../../../../core/services/event/event.interface'; +import { PaginatorComponent } from '../../../../shared/components/paginator/paginator.component'; +import { EventCardComponent } from '../event-card/event-card.component'; + +@Component({ + selector: 'app-event-list', + imports: [EventCardComponent, PaginatorComponent], + templateUrl: './event-list.component.html', + styleUrl: './event-list.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class EventListComponent implements OnInit { + private readonly eventService = inject(EventService); + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly destroyRef = inject(DestroyRef); + + readonly search = input(false); + protected readonly results = signal(null); + protected readonly loading = signal(true); + protected readonly error = signal(false); + protected readonly query = signal(''); + + ngOnInit(): void { + this.route.queryParamMap.pipe( + map((params) => { + const requestedPage = Number(params.get('page')); + return { + page: Number.isSafeInteger(requestedPage) && requestedPage > 0 ? requestedPage : 1, + query: this.search() ? (params.get('q') ?? '').trim() : '', + }; + }), + distinctUntilChanged((a, b) => a.page === b.page && a.query === b.query), + tap(({ query }) => { + this.query.set(query); + this.loading.set(true); + this.error.set(false); + this.results.set(null); + }), + switchMap(({ page, query }) => this.eventService.list(page, query).pipe( + catchError(() => { + this.error.set(true); + return of(null); + }), + )), + takeUntilDestroyed(this.destroyRef), + ).subscribe((results) => { + this.results.set(results); + this.loading.set(false); + }); + } + + protected changePage(page: number): void { + void this.router.navigate([], { + relativeTo: this.route, + queryParams: { page: page === 1 ? null : page }, + queryParamsHandling: 'merge', + }); + } +} diff --git a/src/app/features/store/pages/event-detail-page/event-detail-page.component.html b/src/app/features/store/pages/event-detail-page/event-detail-page.component.html new file mode 100644 index 0000000..c6547f6 --- /dev/null +++ b/src/app/features/store/pages/event-detail-page/event-detail-page.component.html @@ -0,0 +1,19 @@ +← Volver a eventos +@if (event(); as item) { +
+
+ @if (item.image) { + + } +
+
+

{{ item.title }}

+ @if (item.subtitle) {

{{ item.subtitle }}

} + @if (item.date_text) {

{{ item.date_text }}

} + @if (item.location) {

{{ item.location }}

} + @if (item.description) {

{{ item.description }}

} +
+
+} @else if (!loading()) { +

No encontramos este evento.

+} diff --git a/src/app/features/store/pages/event-detail-page/event-detail-page.component.scss b/src/app/features/store/pages/event-detail-page/event-detail-page.component.scss new file mode 100644 index 0000000..5d770f9 --- /dev/null +++ b/src/app/features/store/pages/event-detail-page/event-detail-page.component.scss @@ -0,0 +1,8 @@ +.event-detail__back { display: inline-block; margin-bottom: 2rem; color: var(--tenant-primary); } +.event-detail { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(2rem, 5vw, 5rem); } +.event-detail__image { aspect-ratio: 1.43; background: #e5e6e7; } +.event-detail__image img { width: 100%; height: 100%; object-fit: cover; display: block; } +.event-detail__content h1 { font-size: clamp(1.7rem, 3vw, 2.8rem); text-transform: uppercase; } +.event-detail__subtitle { color: #63666a; } +.event-detail__description { margin-top: 2rem; white-space: pre-line; } +@media (max-width: 767.98px) { .event-detail { grid-template-columns: 1fr; } } diff --git a/src/app/features/store/pages/event-detail-page/event-detail-page.component.ts b/src/app/features/store/pages/event-detail-page/event-detail-page.component.ts new file mode 100644 index 0000000..6d01bc0 --- /dev/null +++ b/src/app/features/store/pages/event-detail-page/event-detail-page.component.ts @@ -0,0 +1,35 @@ +import { ChangeDetectionStrategy, Component, DestroyRef, inject, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ActivatedRoute, RouterLink } from '@angular/router'; +import { catchError, map, of, switchMap } from 'rxjs'; + +import { EventService } from '../../../../core/services/event/event.service'; +import { StoreEvent } from '../../../../core/services/event/event.interface'; + +@Component({ + selector: 'app-event-detail-page', + imports: [RouterLink], + templateUrl: './event-detail-page.component.html', + styleUrl: './event-detail-page.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class EventDetailPageComponent { + private readonly route = inject(ActivatedRoute); + private readonly events = inject(EventService); + private readonly destroyRef = inject(DestroyRef); + protected readonly event = signal(null); + protected readonly loading = signal(true); + + constructor() { + this.route.paramMap.pipe( + map((params) => Number(params.get('id'))), + switchMap((id) => Number.isInteger(id) && id > 0 + ? this.events.get(id).pipe(map((response) => response.data), catchError(() => of(null))) + : of(null)), + takeUntilDestroyed(this.destroyRef), + ).subscribe((event) => { + this.event.set(event); + this.loading.set(false); + }); + } +} diff --git a/src/app/features/store/pages/event-home-page/event-home-page.component.ts b/src/app/features/store/pages/event-home-page/event-home-page.component.ts new file mode 100644 index 0000000..643423b --- /dev/null +++ b/src/app/features/store/pages/event-home-page/event-home-page.component.ts @@ -0,0 +1,10 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { EventListComponent } from '../../components/event-list/event-list.component'; + +@Component({ + selector: 'app-event-home-page', + imports: [EventListComponent], + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class EventHomePageComponent {} diff --git a/src/app/features/store/pages/event-search-page/event-search-page.component.ts b/src/app/features/store/pages/event-search-page/event-search-page.component.ts new file mode 100644 index 0000000..49071ad --- /dev/null +++ b/src/app/features/store/pages/event-search-page/event-search-page.component.ts @@ -0,0 +1,10 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { EventListComponent } from '../../components/event-list/event-list.component'; + +@Component({ + selector: 'app-event-search-page', + imports: [EventListComponent], + template: '

Resultados de búsqueda

', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class EventSearchPageComponent {} diff --git a/src/app/features/store/store.routes.ts b/src/app/features/store/store.routes.ts index dc5719e..0c26600 100644 --- a/src/app/features/store/store.routes.ts +++ b/src/app/features/store/store.routes.ts @@ -1,4 +1,5 @@ -import { Routes } from '@angular/router'; +import { inject } from '@angular/core'; +import { CanMatchFn, Routes } from '@angular/router'; import { SimpleLayoutComponent } from '../../core/layout/simple-layout/simple-layout.component'; import { StoreLayoutComponent } from '../../core/layout/store-layout/store-layout.component'; @@ -13,6 +14,10 @@ import { RegisterPageComponent } from './pages/register-page/register-page.compo import { StoreHomePageComponent } from './pages/store-home-page/store-home-page.component'; import { storeHomeCatalogResolver } from './pages/store-home-page/store-home-page.resolver'; import { checkoutPendingPurchaseGuard } from './pages/checkout-page/checkout-page.guard'; +import { TenantService } from '../../core/services/tenant.service'; + +const isMultiEventStorefront: CanMatchFn = () => + inject(TenantService).tenant()?.storefront_website_type_code === 'onticket_multi_event'; export const routes: Routes = [ { @@ -21,6 +26,14 @@ export const routes: Routes = [ children: [ { path: '', + pathMatch: 'full', + canMatch: [isMultiEventStorefront], + loadComponent: () => + import('./pages/event-home-page/event-home-page.component').then((m) => m.EventHomePageComponent), + }, + { + path: '', + pathMatch: 'full', component: StoreHomePageComponent, canActivate: [hasMenuGuard('index')], resolve: { @@ -68,11 +81,23 @@ export const routes: Routes = [ }, ], }, + { + path: 'buscar', + canMatch: [isMultiEventStorefront], + loadComponent: () => + import('./pages/event-search-page/event-search-page.component').then((m) => m.EventSearchPageComponent), + }, { path: 'buscar', loadComponent: () => import('./pages/search-page/search-page.component').then((m) => m.SearchPageComponent), }, + { + path: 'evento/:id', + canMatch: [isMultiEventStorefront], + loadComponent: () => + import('./pages/event-detail-page/event-detail-page.component').then((m) => m.EventDetailPageComponent), + }, { path: 'categoria/:id', loadComponent: () =>