feat(events): add multi-event storefront pages
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
<a class="event-card" [routerLink]="['/evento', event().id]">
|
||||
<div class="event-card__image">
|
||||
@if (event().image) {
|
||||
<img [src]="event().image" [alt]="event().title" />
|
||||
}
|
||||
</div>
|
||||
<div class="event-card__body">
|
||||
<h2 class="event-card__title">{{ event().title }}</h2>
|
||||
@if (event().subtitle) {
|
||||
<p class="event-card__subtitle">{{ event().subtitle }}</p>
|
||||
}
|
||||
@if (event().date_text) {
|
||||
<p class="event-card__meta"><i class="fa-regular fa-calendar" aria-hidden="true"></i>{{ event().date_text }}</p>
|
||||
}
|
||||
@if (event().location) {
|
||||
<p class="event-card__meta"><i class="fa-solid fa-location-dot" aria-hidden="true"></i>{{ event().location }}</p>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
@@ -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; }
|
||||
@@ -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<StoreEvent>();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
@if (loading()) {
|
||||
<p class="event-list__message">Cargando eventos...</p>
|
||||
} @else if (error()) {
|
||||
<p class="event-list__message" role="alert">No pudimos cargar los eventos.</p>
|
||||
} @else if (results(); as page) {
|
||||
@if (page.data.length) {
|
||||
<div class="event-list__grid">
|
||||
@for (event of page.data; track event.id) {
|
||||
<app-event-card [event]="event" />
|
||||
}
|
||||
</div>
|
||||
@if (page.meta.last_page > 1) {
|
||||
<div class="event-list__pagination">
|
||||
<app-paginator [page]="page.meta.current_page" [totalPages]="page.meta.last_page" (pageChange)="changePage($event)" />
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<p class="event-list__message">{{ search() ? 'No encontramos eventos para tu búsqueda.' : 'No hay eventos disponibles en este momento.' }}</p>
|
||||
}
|
||||
}
|
||||
@@ -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; } }
|
||||
@@ -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<StoreEventPage | null>(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',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<a routerLink="/" class="event-detail__back">← Volver a eventos</a>
|
||||
@if (event(); as item) {
|
||||
<article class="event-detail">
|
||||
<div class="event-detail__image">
|
||||
@if (item.image) {
|
||||
<img [src]="item.image" [alt]="item.title" />
|
||||
}
|
||||
</div>
|
||||
<div class="event-detail__content">
|
||||
<h1>{{ item.title }}</h1>
|
||||
@if (item.subtitle) { <p class="event-detail__subtitle">{{ item.subtitle }}</p> }
|
||||
@if (item.date_text) { <p><i class="fa-regular fa-calendar" aria-hidden="true"></i> {{ item.date_text }}</p> }
|
||||
@if (item.location) { <p><i class="fa-solid fa-location-dot" aria-hidden="true"></i> {{ item.location }}</p> }
|
||||
@if (item.description) { <p class="event-detail__description">{{ item.description }}</p> }
|
||||
</div>
|
||||
</article>
|
||||
} @else if (!loading()) {
|
||||
<p role="alert">No encontramos este evento.</p>
|
||||
}
|
||||
@@ -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; } }
|
||||
@@ -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<StoreEvent | null>(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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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: '<app-event-list />',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class EventHomePageComponent {}
|
||||
@@ -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: '<section aria-labelledby="event-search-title"><h1 id="event-search-title" class="h3 mb-4">Resultados de búsqueda</h1><app-event-list [search]="true" /></section>',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class EventSearchPageComponent {}
|
||||
@@ -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: () =>
|
||||
|
||||
Reference in New Issue
Block a user