feat: build and wire the Quick View modal
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
The Quick View button (search results page only) emitted quickViewPlaceholder with zero listeners anywhere up the chain - the button did nothing. Built a minimal QuickViewDialogComponent (image, name, price incl. discount, short description, Add to Cart, link to full product page) and wired the event through product-grid -> search-results -> catalog-container, which fetches the product via ProductFacade and opens the dialog. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
(selected)="productSelected.emit(product)"
|
||||
(addToCart)="onAddToCart(product, $event.event)"
|
||||
(preview)="productPreview.emit($event)"
|
||||
(quickViewPlaceholder)="quickView.emit($event)"
|
||||
(favoriteToggled)="favoriteToggled.emit(product)"
|
||||
(compareToggled)="compareToggled.emit(product)"
|
||||
(shareRequested)="shareRequested.emit(product)"
|
||||
|
||||
@@ -28,6 +28,7 @@ export class CatalogProductGridComponent {
|
||||
@Output() productSelected = new EventEmitter<Product>();
|
||||
@Output() addToCart = new EventEmitter<{ product: Product; event: Event }>();
|
||||
@Output() productPreview = new EventEmitter<number>();
|
||||
@Output() quickView = new EventEmitter<number>();
|
||||
@Output() favoriteToggled = new EventEmitter<Product>();
|
||||
@Output() compareToggled = new EventEmitter<Product>();
|
||||
@Output() shareRequested = new EventEmitter<Product>();
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
(productSelected)="productSelected.emit($event)"
|
||||
(addToCart)="addToCart.emit($event)"
|
||||
(productPreview)="productPreview.emit($event)"
|
||||
(quickView)="quickView.emit($event)"
|
||||
(favoriteToggled)="favoriteToggled.emit($event)"
|
||||
(compareToggled)="compareToggled.emit($event)"
|
||||
(shareRequested)="shareRequested.emit($event)" />
|
||||
|
||||
@@ -40,6 +40,7 @@ export class CatalogSearchResultsComponent {
|
||||
@Output() productSelected = new EventEmitter<Product>();
|
||||
@Output() addToCart = new EventEmitter<{ product: Product; event: Event }>();
|
||||
@Output() productPreview = new EventEmitter<number>();
|
||||
@Output() quickView = new EventEmitter<number>();
|
||||
@Output() favoriteToggled = new EventEmitter<Product>();
|
||||
@Output() compareToggled = new EventEmitter<Product>();
|
||||
@Output() shareRequested = new EventEmitter<Product>();
|
||||
|
||||
@@ -229,6 +229,7 @@
|
||||
(productSelected)="selectProduct($event)"
|
||||
(addToCart)="addToCart($event)"
|
||||
(productPreview)="previewProduct($event)"
|
||||
(quickView)="openQuickView($event)"
|
||||
(favoriteToggled)="onFavoriteToggled($event)"
|
||||
(compareToggled)="onCompareToggled($event)"
|
||||
(shareRequested)="onShareRequested($event)" />
|
||||
@@ -295,3 +296,9 @@
|
||||
}
|
||||
}
|
||||
</main>
|
||||
|
||||
<app-quick-view-dialog
|
||||
[product]="quickViewProduct()"
|
||||
[loading]="quickViewLoading()"
|
||||
(closed)="closeQuickView()"
|
||||
(addToCart)="addQuickViewToCart($event)" />
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Product } from '../../../../core/products/models/product-domain.model';
|
||||
import { ConfigService } from '../../../../core/config/config.service';
|
||||
import { FeatureConfigService } from '../../../../core/config/feature-config.service';
|
||||
import { CategoryFacade } from '../../../../facades/platform/category.facade';
|
||||
import { ProductFacade } from '../../../../facades/platform/product.facade';
|
||||
import { SearchFacade } from '../../../../facades/platform/search.facade';
|
||||
import { UserExperienceFacade } from '../../../../facades/platform/user-experience.facade';
|
||||
import { CartService } from '../../../../services';
|
||||
@@ -32,6 +33,7 @@ import { EmptyStateComponent } from '../../../../shared/ui/empty-state/empty-sta
|
||||
import { SkeletonComponent } from '../../../../shared/ui/skeleton/skeleton.component';
|
||||
import { ButtonComponent } from '../../../../shared/ui/button/button.component';
|
||||
import { IconComponent } from '../../../../shared/ui/icon/icon.component';
|
||||
import { QuickViewDialogComponent } from '../../product/components/quick-view-dialog/quick-view-dialog.component';
|
||||
import { CatalogState, createInitialCatalogState } from '../models/catalog-state.model';
|
||||
import { ProductShareService } from '../../user-experience/services/product-share.service';
|
||||
import { UserNotificationService } from '../../user-experience/services/user-notification.service';
|
||||
@@ -56,7 +58,8 @@ type CatalogLoadingStrategy = 'pagination' | 'loadMore' | 'infiniteScroll';
|
||||
EmptyStateComponent,
|
||||
SkeletonComponent,
|
||||
ButtonComponent,
|
||||
IconComponent
|
||||
IconComponent,
|
||||
QuickViewDialogComponent
|
||||
],
|
||||
templateUrl: './catalog-container.component.html',
|
||||
styleUrls: ['./catalog-container.component.scss'],
|
||||
@@ -68,6 +71,7 @@ export class CatalogContainerComponent {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly configService = inject(ConfigService);
|
||||
private readonly categoryFacade = inject(CategoryFacade);
|
||||
private readonly productFacade = inject(ProductFacade);
|
||||
private readonly searchFacade = inject(SearchFacade);
|
||||
private readonly featureConfig = inject(FeatureConfigService);
|
||||
private readonly cartService = inject(CartService);
|
||||
@@ -287,6 +291,31 @@ export class CatalogContainerComponent {
|
||||
this.prefetchService.prefetchItem(productId);
|
||||
}
|
||||
|
||||
readonly quickViewProduct = signal<Product | null>(null);
|
||||
readonly quickViewLoading = signal(false);
|
||||
|
||||
openQuickView(productId: number): void {
|
||||
this.quickViewProduct.set(null);
|
||||
this.quickViewLoading.set(true);
|
||||
this.productFacade.getProduct(productId).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
||||
next: product => {
|
||||
this.quickViewProduct.set(product);
|
||||
this.quickViewLoading.set(false);
|
||||
},
|
||||
error: () => this.quickViewLoading.set(false)
|
||||
});
|
||||
}
|
||||
|
||||
closeQuickView(): void {
|
||||
this.quickViewProduct.set(null);
|
||||
this.quickViewLoading.set(false);
|
||||
}
|
||||
|
||||
addQuickViewToCart(product: Product): void {
|
||||
this.cartService.addItem(product.itemID);
|
||||
this.closeQuickView();
|
||||
}
|
||||
|
||||
retry(): void {
|
||||
this.enterCategory(this.state().category?.id ?? null);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<app-dialog
|
||||
[open]="!!product || loading"
|
||||
[titleText]="'catalog.quickView' | translate"
|
||||
size="md"
|
||||
(closed)="closed.emit()"
|
||||
>
|
||||
@if (loading) {
|
||||
<div class="quick-view__loading" role="status" aria-live="polite">{{ 'common.loading' | translate }}</div>
|
||||
} @else if (product) {
|
||||
<div class="quick-view">
|
||||
<img class="quick-view__image" [src]="mainImage" [alt]="product.name" />
|
||||
<div class="quick-view__body">
|
||||
<h3 class="quick-view__title">{{ product.name }}</h3>
|
||||
<p class="quick-view__price">
|
||||
@if (hasDiscount) {
|
||||
<span class="quick-view__price-original">{{ product.price | number:'1.2-2' }} {{ product.currency }}</span>
|
||||
<span class="quick-view__price-final">{{ discountedPrice | number:'1.2-2' }} {{ product.currency }}</span>
|
||||
} @else {
|
||||
<span class="quick-view__price-final">{{ product.price | number:'1.2-2' }} {{ product.currency }}</span>
|
||||
}
|
||||
</p>
|
||||
@if (product.simpleDescription) {
|
||||
<p class="quick-view__description">{{ product.simpleDescription }}</p>
|
||||
}
|
||||
<div class="quick-view__actions">
|
||||
<app-button variant="primary" (click)="addToCart.emit(product)">{{ 'carousel.addToCart' | translate }}</app-button>
|
||||
<a class="quick-view__link" [routerLink]="('/product/' + product.itemID) | langRoute" (click)="closed.emit()">
|
||||
{{ 'catalog.quickViewDetails' | translate }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</app-dialog>
|
||||
@@ -0,0 +1,73 @@
|
||||
.quick-view {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
|
||||
@media (min-width: 560px) {
|
||||
grid-template-columns: 200px 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-view__image {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
object-fit: cover;
|
||||
border-radius: var(--radius-md, 8px);
|
||||
background: var(--bg-secondary, #f3f3f3);
|
||||
}
|
||||
|
||||
.quick-view__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.quick-view__title {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-lg, 1.125rem);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.quick-view__price {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.quick-view__price-original {
|
||||
text-decoration: line-through;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm, 0.875rem);
|
||||
}
|
||||
|
||||
.quick-view__price-final {
|
||||
font-weight: var(--font-weight-bold, 700);
|
||||
font-size: var(--font-size-lg, 1.125rem);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.quick-view__description {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm, 0.875rem);
|
||||
}
|
||||
|
||||
.quick-view__actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.quick-view__link {
|
||||
text-align: center;
|
||||
color: var(--primary-color);
|
||||
font-size: var(--font-size-sm, 0.875rem);
|
||||
}
|
||||
|
||||
.quick-view__loading {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { DecimalPipe } from '@angular/common';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { Product } from '../../../../../core/products/models/product-domain.model';
|
||||
import { DialogComponent } from '../../../../../shared/ui/dialog/dialog.component';
|
||||
import { ButtonComponent } from '../../../../../shared/ui/button/button.component';
|
||||
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
||||
import { LangRoutePipe } from '../../../../../pipes/lang-route.pipe';
|
||||
import { getDiscountedPrice, getMainImage } from '../../../../../utils/item.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-quick-view-dialog',
|
||||
standalone: true,
|
||||
imports: [DialogComponent, ButtonComponent, TranslatePipe, LangRoutePipe, DecimalPipe, RouterLink],
|
||||
templateUrl: './quick-view-dialog.component.html',
|
||||
styleUrl: './quick-view-dialog.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class QuickViewDialogComponent {
|
||||
@Input() product: Product | null = null;
|
||||
@Input() loading = false;
|
||||
|
||||
@Output() closed = new EventEmitter<void>();
|
||||
@Output() addToCart = new EventEmitter<Product>();
|
||||
|
||||
get mainImage(): string {
|
||||
return this.product ? getMainImage(this.product) : '';
|
||||
}
|
||||
|
||||
get discountedPrice(): number {
|
||||
return this.product ? getDiscountedPrice(this.product) : 0;
|
||||
}
|
||||
|
||||
get hasDiscount(): boolean {
|
||||
return !!this.product?.discount && this.product.discount > 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user