diff --git a/src/app/features/website/catalog/components/product-grid/product-grid.component.html b/src/app/features/website/catalog/components/product-grid/product-grid.component.html index 230427b..940fd3d 100644 --- a/src/app/features/website/catalog/components/product-grid/product-grid.component.html +++ b/src/app/features/website/catalog/components/product-grid/product-grid.component.html @@ -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)" diff --git a/src/app/features/website/catalog/components/product-grid/product-grid.component.ts b/src/app/features/website/catalog/components/product-grid/product-grid.component.ts index 79a8ed2..b005b5d 100644 --- a/src/app/features/website/catalog/components/product-grid/product-grid.component.ts +++ b/src/app/features/website/catalog/components/product-grid/product-grid.component.ts @@ -28,6 +28,7 @@ export class CatalogProductGridComponent { @Output() productSelected = new EventEmitter(); @Output() addToCart = new EventEmitter<{ product: Product; event: Event }>(); @Output() productPreview = new EventEmitter(); + @Output() quickView = new EventEmitter(); @Output() favoriteToggled = new EventEmitter(); @Output() compareToggled = new EventEmitter(); @Output() shareRequested = new EventEmitter(); diff --git a/src/app/features/website/catalog/components/search-results/search-results.component.html b/src/app/features/website/catalog/components/search-results/search-results.component.html index 1cda4f9..7817b45 100644 --- a/src/app/features/website/catalog/components/search-results/search-results.component.html +++ b/src/app/features/website/catalog/components/search-results/search-results.component.html @@ -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)" /> diff --git a/src/app/features/website/catalog/components/search-results/search-results.component.ts b/src/app/features/website/catalog/components/search-results/search-results.component.ts index 59f57b5..0c5aa63 100644 --- a/src/app/features/website/catalog/components/search-results/search-results.component.ts +++ b/src/app/features/website/catalog/components/search-results/search-results.component.ts @@ -40,6 +40,7 @@ export class CatalogSearchResultsComponent { @Output() productSelected = new EventEmitter(); @Output() addToCart = new EventEmitter<{ product: Product; event: Event }>(); @Output() productPreview = new EventEmitter(); + @Output() quickView = new EventEmitter(); @Output() favoriteToggled = new EventEmitter(); @Output() compareToggled = new EventEmitter(); @Output() shareRequested = new EventEmitter(); diff --git a/src/app/features/website/catalog/containers/catalog-container.component.html b/src/app/features/website/catalog/containers/catalog-container.component.html index 777d9a6..7be50fb 100644 --- a/src/app/features/website/catalog/containers/catalog-container.component.html +++ b/src/app/features/website/catalog/containers/catalog-container.component.html @@ -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 @@ } } + + diff --git a/src/app/features/website/catalog/containers/catalog-container.component.ts b/src/app/features/website/catalog/containers/catalog-container.component.ts index a59ceeb..c56ced6 100644 --- a/src/app/features/website/catalog/containers/catalog-container.component.ts +++ b/src/app/features/website/catalog/containers/catalog-container.component.ts @@ -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(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); } diff --git a/src/app/features/website/product/components/quick-view-dialog/quick-view-dialog.component.html b/src/app/features/website/product/components/quick-view-dialog/quick-view-dialog.component.html new file mode 100644 index 0000000..9355a60 --- /dev/null +++ b/src/app/features/website/product/components/quick-view-dialog/quick-view-dialog.component.html @@ -0,0 +1,34 @@ + + @if (loading) { +
{{ 'common.loading' | translate }}
+ } @else if (product) { +
+ +
+

{{ product.name }}

+

+ @if (hasDiscount) { + {{ product.price | number:'1.2-2' }} {{ product.currency }} + {{ discountedPrice | number:'1.2-2' }} {{ product.currency }} + } @else { + {{ product.price | number:'1.2-2' }} {{ product.currency }} + } +

+ @if (product.simpleDescription) { +

{{ product.simpleDescription }}

+ } +
+ {{ 'carousel.addToCart' | translate }} + + {{ 'catalog.quickViewDetails' | translate }} + +
+
+
+ } +
diff --git a/src/app/features/website/product/components/quick-view-dialog/quick-view-dialog.component.scss b/src/app/features/website/product/components/quick-view-dialog/quick-view-dialog.component.scss new file mode 100644 index 0000000..67c74e9 --- /dev/null +++ b/src/app/features/website/product/components/quick-view-dialog/quick-view-dialog.component.scss @@ -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); +} diff --git a/src/app/features/website/product/components/quick-view-dialog/quick-view-dialog.component.ts b/src/app/features/website/product/components/quick-view-dialog/quick-view-dialog.component.ts new file mode 100644 index 0000000..b66a7b5 --- /dev/null +++ b/src/app/features/website/product/components/quick-view-dialog/quick-view-dialog.component.ts @@ -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(); + @Output() addToCart = new EventEmitter(); + + 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; + } +} diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index ebfdf25..ad5cc77 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -273,6 +273,7 @@ export const en: Translations = { compare: 'Compare', share: 'Share', quickView: 'Quick view', + quickViewDetails: 'View full details', stockHigh: 'In stock', stockMedium: 'Limited stock', stockLow: 'Almost gone', diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index 0aed06c..c3f4963 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -273,6 +273,7 @@ export const hy: Translations = { compare: 'Համեմատել', share: 'Կիսվել', quickView: 'Արագ դիտում', + quickViewDetails: 'Տեսնել ամբողջությամբ', stockHigh: 'Առկա է', stockMedium: 'Սահմանափակ քանակ', stockLow: 'Գրեթե սպառված է', diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index 61a1d11..7503dc2 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -273,6 +273,7 @@ export const ru: Translations = { compare: 'Сравнить', share: 'Поделиться', quickView: 'Быстрый просмотр', + quickViewDetails: 'Смотреть полностью', stockHigh: 'В наличии', stockMedium: 'Ограниченно', stockLow: 'Почти распродано', diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index ca90cfa..9304c16 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -271,6 +271,7 @@ export interface Translations { compare: string; share: string; quickView: string; + quickViewDetails: string; stockHigh: string; stockMedium: string; stockLow: string;