diff --git a/angular.json b/angular.json index 0698c77..edace0f 100644 --- a/angular.json +++ b/angular.json @@ -146,7 +146,8 @@ }, "sourceMap": false, "namedChunks": false, - "extractLicenses": true + "extractLicenses": true, + "serviceWorker": "ngsw-config.json" } }, "defaultConfiguration": "production" diff --git a/ngsw-config.json b/ngsw-config.json index 14e0154..a949b9a 100644 --- a/ngsw-config.json +++ b/ngsw-config.json @@ -8,7 +8,6 @@ "resources": { "files": [ "/favicon.ico", - "/index.csr.html", "/index.html", "/manifest.webmanifest", "/*.css", @@ -48,7 +47,7 @@ "https://**/*.webp" ], "cacheConfig": { - "maxSize": 50, + "maxSize": 200, "maxAge": "7d", "strategy": "performance" } diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 35bef9f..773a15b 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -16,9 +16,10 @@ export const appConfig: ApplicationConfig = { ), provideHttpClient( withInterceptors([cacheInterceptor]) - ), provideServiceWorker('ngsw-worker.js', { - enabled: !isDevMode(), - registrationStrategy: 'registerWhenStable:30000' - }) + ), + provideServiceWorker('ngsw-worker.js', { + enabled: !isDevMode(), + registrationStrategy: 'registerWhenStable:30000' + }) ] -} +}; diff --git a/src/app/app.ts b/src/app/app.ts index 9563e2f..156e7bd 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -43,7 +43,8 @@ export class App implements OnInit { takeUntilDestroyed(this.destroyRef) ) .subscribe((event) => { - const url = (event as NavigationEnd).urlAfterRedirects || (event as NavigationEnd).url; + const navEnd = event as NavigationEnd; + const url = navEnd.urlAfterRedirects || navEnd.url; this.isHomePage.set(url === '/' || url === '/home' || url === ''); }); } diff --git a/src/app/brands/dexar/pages/info/about/about.component.html b/src/app/brands/dexar/pages/info/about/about.component.html deleted file mode 100644 index 5a7d436..0000000 --- a/src/app/brands/dexar/pages/info/about/about.component.html +++ /dev/null @@ -1,74 +0,0 @@ - diff --git a/src/app/brands/dexar/pages/info/contacts/contacts.component.html b/src/app/brands/dexar/pages/info/contacts/contacts.component.html deleted file mode 100644 index 4933fc4..0000000 --- a/src/app/brands/dexar/pages/info/contacts/contacts.component.html +++ /dev/null @@ -1,46 +0,0 @@ - diff --git a/src/app/brands/dexar/pages/info/delivery/delivery.component.html b/src/app/brands/dexar/pages/info/delivery/delivery.component.html deleted file mode 100644 index 4da78d5..0000000 --- a/src/app/brands/dexar/pages/info/delivery/delivery.component.html +++ /dev/null @@ -1,60 +0,0 @@ - diff --git a/src/app/brands/dexar/pages/info/faq/faq.component.html b/src/app/brands/dexar/pages/info/faq/faq.component.html deleted file mode 100644 index 9dac502..0000000 --- a/src/app/brands/dexar/pages/info/faq/faq.component.html +++ /dev/null @@ -1,244 +0,0 @@ - diff --git a/src/app/brands/dexar/pages/info/guarantee/guarantee.component.html b/src/app/brands/dexar/pages/info/guarantee/guarantee.component.html deleted file mode 100644 index 36c1cc0..0000000 --- a/src/app/brands/dexar/pages/info/guarantee/guarantee.component.html +++ /dev/null @@ -1,158 +0,0 @@ - diff --git a/src/app/brands/dexar/pages/legal/company-details/company-details.component.html b/src/app/brands/dexar/pages/legal/company-details/company-details.component.html deleted file mode 100644 index 7ae6a29..0000000 --- a/src/app/brands/dexar/pages/legal/company-details/company-details.component.html +++ /dev/null @@ -1,102 +0,0 @@ - diff --git a/src/app/brands/dexar/pages/legal/payment-terms/payment-terms.component.html b/src/app/brands/dexar/pages/legal/payment-terms/payment-terms.component.html deleted file mode 100644 index a65e258..0000000 --- a/src/app/brands/dexar/pages/legal/payment-terms/payment-terms.component.html +++ /dev/null @@ -1,119 +0,0 @@ - diff --git a/src/app/brands/dexar/pages/legal/privacy-policy/privacy-policy.component.html b/src/app/brands/dexar/pages/legal/privacy-policy/privacy-policy.component.html deleted file mode 100644 index cd47b3f..0000000 --- a/src/app/brands/dexar/pages/legal/privacy-policy/privacy-policy.component.html +++ /dev/null @@ -1,258 +0,0 @@ - diff --git a/src/app/brands/dexar/pages/legal/public-offer/public-offer.component.html b/src/app/brands/dexar/pages/legal/public-offer/public-offer.component.html deleted file mode 100644 index 4a7440a..0000000 --- a/src/app/brands/dexar/pages/legal/public-offer/public-offer.component.html +++ /dev/null @@ -1,465 +0,0 @@ - diff --git a/src/app/brands/dexar/pages/legal/return-policy/return-policy.component.html b/src/app/brands/dexar/pages/legal/return-policy/return-policy.component.html deleted file mode 100644 index 7863b7c..0000000 --- a/src/app/brands/dexar/pages/legal/return-policy/return-policy.component.html +++ /dev/null @@ -1,134 +0,0 @@ - diff --git a/src/app/brands/novo/pages/info/about/about.component.ts b/src/app/brands/novo/pages/info/about/about.component.ts index 858280e..b62b66b 100644 --- a/src/app/brands/novo/pages/info/about/about.component.ts +++ b/src/app/brands/novo/pages/info/about/about.component.ts @@ -1,9 +1,10 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'app-about-novo', imports: [], templateUrl: './about.component.html', - styleUrls: ['../../../../../pages/info/about/about.component.scss'] + styleUrls: ['../../../../../pages/info/about/about.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class AboutNovoComponent {} diff --git a/src/app/brands/novo/pages/info/contacts/contacts.component.ts b/src/app/brands/novo/pages/info/contacts/contacts.component.ts index c87b3ef..805c1ed 100644 --- a/src/app/brands/novo/pages/info/contacts/contacts.component.ts +++ b/src/app/brands/novo/pages/info/contacts/contacts.component.ts @@ -1,9 +1,10 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'app-contacts-novo', imports: [], templateUrl: './contacts.component.html', - styleUrls: ['../../../../../pages/info/contacts/contacts.component.scss'] + styleUrls: ['../../../../../pages/info/contacts/contacts.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class ContactsNovoComponent {} diff --git a/src/app/brands/novo/pages/info/delivery/delivery.component.ts b/src/app/brands/novo/pages/info/delivery/delivery.component.ts index 971a09e..cb56195 100644 --- a/src/app/brands/novo/pages/info/delivery/delivery.component.ts +++ b/src/app/brands/novo/pages/info/delivery/delivery.component.ts @@ -1,9 +1,10 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'app-delivery-novo', imports: [], templateUrl: './delivery.component.html', - styleUrls: ['../../../../../pages/info/delivery/delivery.component.scss'] + styleUrls: ['../../../../../pages/info/delivery/delivery.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class DeliveryNovoComponent {} diff --git a/src/app/brands/novo/pages/info/faq/faq.component.ts b/src/app/brands/novo/pages/info/faq/faq.component.ts index 9294424..d5d55e3 100644 --- a/src/app/brands/novo/pages/info/faq/faq.component.ts +++ b/src/app/brands/novo/pages/info/faq/faq.component.ts @@ -1,9 +1,10 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'app-faq-novo', imports: [], templateUrl: './faq.component.html', - styleUrls: ['../../../../../pages/info/faq/faq.component.scss'] + styleUrls: ['../../../../../pages/info/faq/faq.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class FaqNovoComponent {} diff --git a/src/app/brands/novo/pages/info/guarantee/guarantee.component.ts b/src/app/brands/novo/pages/info/guarantee/guarantee.component.ts index 08877bb..b1ac45c 100644 --- a/src/app/brands/novo/pages/info/guarantee/guarantee.component.ts +++ b/src/app/brands/novo/pages/info/guarantee/guarantee.component.ts @@ -1,9 +1,10 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'app-guarantee-novo', imports: [], templateUrl: './guarantee.component.html', - styleUrls: ['../../../../../pages/info/guarantee/guarantee.component.scss'] + styleUrls: ['../../../../../pages/info/guarantee/guarantee.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class GuaranteeNovoComponent {} diff --git a/src/app/brands/novo/pages/legal/company-details/company-details.component.ts b/src/app/brands/novo/pages/legal/company-details/company-details.component.ts index 79b17e9..648fe40 100644 --- a/src/app/brands/novo/pages/legal/company-details/company-details.component.ts +++ b/src/app/brands/novo/pages/legal/company-details/company-details.component.ts @@ -1,9 +1,10 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'app-company-details-novo', imports: [], templateUrl: './company-details.component.html', - styleUrls: ['../../../../../pages/legal/company-details/company-details.component.scss'] + styleUrls: ['../../../../../pages/legal/company-details/company-details.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class CompanyDetailsNovoComponent {} diff --git a/src/app/brands/novo/pages/legal/payment-terms/payment-terms.component.ts b/src/app/brands/novo/pages/legal/payment-terms/payment-terms.component.ts index 995ccb0..a149b22 100644 --- a/src/app/brands/novo/pages/legal/payment-terms/payment-terms.component.ts +++ b/src/app/brands/novo/pages/legal/payment-terms/payment-terms.component.ts @@ -1,9 +1,10 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'app-payment-terms-novo', imports: [], templateUrl: './payment-terms.component.html', - styleUrls: ['../../../../../pages/legal/payment-terms/payment-terms.component.scss'] + styleUrls: ['../../../../../pages/legal/payment-terms/payment-terms.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class PaymentTermsNovoComponent {} diff --git a/src/app/brands/novo/pages/legal/privacy-policy/privacy-policy.component.ts b/src/app/brands/novo/pages/legal/privacy-policy/privacy-policy.component.ts index de07fe2..f56b57d 100644 --- a/src/app/brands/novo/pages/legal/privacy-policy/privacy-policy.component.ts +++ b/src/app/brands/novo/pages/legal/privacy-policy/privacy-policy.component.ts @@ -1,9 +1,10 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'app-privacy-policy-novo', imports: [], templateUrl: './privacy-policy.component.html', - styleUrls: ['../../../../../pages/legal/privacy-policy/privacy-policy.component.scss'] + styleUrls: ['../../../../../pages/legal/privacy-policy/privacy-policy.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class PrivacyPolicyNovoComponent {} diff --git a/src/app/brands/novo/pages/legal/public-offer/public-offer.component.ts b/src/app/brands/novo/pages/legal/public-offer/public-offer.component.ts index 853e80a..0aa0542 100644 --- a/src/app/brands/novo/pages/legal/public-offer/public-offer.component.ts +++ b/src/app/brands/novo/pages/legal/public-offer/public-offer.component.ts @@ -1,9 +1,10 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'app-public-offer-novo', imports: [], templateUrl: './public-offer.component.html', - styleUrls: ['../../../../../pages/legal/public-offer/public-offer.component.scss'] + styleUrls: ['../../../../../pages/legal/public-offer/public-offer.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class PublicOfferNovoComponent {} diff --git a/src/app/brands/novo/pages/legal/return-policy/return-policy.component.ts b/src/app/brands/novo/pages/legal/return-policy/return-policy.component.ts index a9b3213..aca0b2d 100644 --- a/src/app/brands/novo/pages/legal/return-policy/return-policy.component.ts +++ b/src/app/brands/novo/pages/legal/return-policy/return-policy.component.ts @@ -1,9 +1,10 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'app-return-policy-novo', imports: [], templateUrl: './return-policy.component.html', - styleUrls: ['../../../../../pages/legal/return-policy/return-policy.component.scss'] + styleUrls: ['../../../../../pages/legal/return-policy/return-policy.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class ReturnPolicyNovoComponent {} diff --git a/src/app/components/back-button/back-button.component.ts b/src/app/components/back-button/back-button.component.ts index 217a8df..4eae8f8 100644 --- a/src/app/components/back-button/back-button.component.ts +++ b/src/app/components/back-button/back-button.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; import { Location } from '@angular/common'; import { environment } from '../../../environments/environment'; @@ -55,7 +55,8 @@ import { environment } from '../../../environments/environment'; } } } - `] + `], + changeDetection: ChangeDetectionStrategy.OnPush }) export class BackButtonComponent { isnovo = environment.theme === 'novo'; diff --git a/src/app/components/empty-cart-icon/empty-cart-icon.component.ts b/src/app/components/empty-cart-icon/empty-cart-icon.component.ts index 2d023a9..0490eb1 100644 --- a/src/app/components/empty-cart-icon/empty-cart-icon.component.ts +++ b/src/app/components/empty-cart-icon/empty-cart-icon.component.ts @@ -1,8 +1,9 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'app-empty-cart-icon', templateUrl: './empty-cart-icon.component.html', - styleUrls: ['./empty-cart-icon.component.scss'] + styleUrls: ['./empty-cart-icon.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class EmptyCartIconComponent {} diff --git a/src/app/components/header/header.component.ts b/src/app/components/header/header.component.ts index 029ef54..4dce072 100644 --- a/src/app/components/header/header.component.ts +++ b/src/app/components/header/header.component.ts @@ -1,4 +1,4 @@ -import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { Component, ChangeDetectionStrategy, Renderer2, inject, DOCUMENT } from '@angular/core'; import { Router, RouterLink, RouterLinkActive } from '@angular/router'; import { CartService } from '../../services'; import { environment } from '../../../environments/environment'; @@ -19,18 +19,25 @@ export class HeaderComponent { logo = environment.logo; isnovo = environment.theme === 'novo'; + private renderer = inject(Renderer2); + private document = inject(DOCUMENT); + constructor(private cartService: CartService, private router: Router) { this.cartItemCount = this.cartService.itemCount; } toggleMenu(): void { this.menuOpen = !this.menuOpen; - document.body.classList.toggle('dexar-menu-open', this.menuOpen); + if (this.menuOpen) { + this.renderer.addClass(this.document.body, 'dexar-menu-open'); + } else { + this.renderer.removeClass(this.document.body, 'dexar-menu-open'); + } } closeMenu(): void { this.menuOpen = false; - document.body.classList.remove('dexar-menu-open'); + this.renderer.removeClass(this.document.body, 'dexar-menu-open'); } navigateToSearch(): void { @@ -41,7 +48,7 @@ export class HeaderComponent { this.closeMenu(); this.router.navigate(['/']).then(() => { setTimeout(() => { - document.getElementById('catalog')?.scrollIntoView({ behavior: 'smooth' }); + this.document.getElementById('catalog')?.scrollIntoView({ behavior: 'smooth' }); }, 100); }); } diff --git a/src/app/components/items-carousel/items-carousel.component.ts b/src/app/components/items-carousel/items-carousel.component.ts index 6773297..49c8fe0 100644 --- a/src/app/components/items-carousel/items-carousel.component.ts +++ b/src/app/components/items-carousel/items-carousel.component.ts @@ -21,7 +21,7 @@ export class ItemsCarouselComponent implements OnInit { loading = signal(true); isnovo = environment.theme === 'novo'; - responsiveOptions: any[] | undefined; + responsiveOptions: { breakpoint: string; numVisible: number; numScroll: number }[] | undefined; constructor( private apiService: ApiService, @@ -68,7 +68,7 @@ export class ItemsCarouselComponent implements OnInit { ]; } - getSeverity(remainings: string) { + getSeverity(remainings: string): 'success' | 'info' | 'warn' | 'danger' | 'secondary' | 'contrast' { switch (remainings) { case 'high': return 'success'; diff --git a/src/app/components/logo/logo.component.ts b/src/app/components/logo/logo.component.ts index 18afc32..10777a4 100644 --- a/src/app/components/logo/logo.component.ts +++ b/src/app/components/logo/logo.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; import { environment } from '../../../environments/environment'; @Component({ @@ -10,7 +10,8 @@ import { environment } from '../../../environments/environment'; height: 100%; object-fit: contain; } - `] + `], + changeDetection: ChangeDetectionStrategy.OnPush }) export class LogoComponent { brandName = environment.brandName; diff --git a/src/app/interceptors/cache.interceptor.ts b/src/app/interceptors/cache.interceptor.ts index 91814b7..8b56727 100644 --- a/src/app/interceptors/cache.interceptor.ts +++ b/src/app/interceptors/cache.interceptor.ts @@ -2,7 +2,7 @@ import { HttpInterceptorFn, HttpResponse } from '@angular/common/http'; import { of } from 'rxjs'; import { tap } from 'rxjs/operators'; -const cache = new Map, timestamp: number }>(); +const cache = new Map, timestamp: number }>(); const CACHE_DURATION = 5 * 60 * 1000; // 5 минут export const cacheInterceptor: HttpInterceptorFn = (req, next) => { @@ -17,6 +17,9 @@ export const cacheInterceptor: HttpInterceptorFn = (req, next) => { return next(req); } + // Cleanup expired entries before checking + cleanupExpiredCache(); + const cachedResponse = cache.get(req.url); // Проверяем наличие и актуальность кэша @@ -25,7 +28,6 @@ export const cacheInterceptor: HttpInterceptorFn = (req, next) => { if (age < CACHE_DURATION) { return of(cachedResponse.response.clone()); } else { - // Кэш устарел, удаляем cache.delete(req.url); } } @@ -43,19 +45,16 @@ export const cacheInterceptor: HttpInterceptorFn = (req, next) => { ); }; -// Функция для очистки кэша (можно использовать при необходимости) +/** Clear all cached responses */ export function clearCache(): void { cache.clear(); -// console.log('[Cache] Cache cleared'); } -// Функция для очистки устаревшего кэша -export function cleanupExpiredCache(): void { +function cleanupExpiredCache(): void { const now = Date.now(); for (const [url, data] of cache.entries()) { if (now - data.timestamp >= CACHE_DURATION) { cache.delete(url); } } -// console.log('[Cache] Expired cache cleaned up'); } diff --git a/src/app/models/category.model.ts b/src/app/models/category.model.ts index 9b4501a..ee3f477 100644 --- a/src/app/models/category.model.ts +++ b/src/app/models/category.model.ts @@ -3,7 +3,7 @@ export interface Category { name: string; parentID: number; icon?: string; - wideBanner?: string | boolean; + wideBanner?: string; itemCount?: number; priority?: number; } diff --git a/src/app/models/item.model.ts b/src/app/models/item.model.ts index 6320ed0..f757e15 100644 --- a/src/app/models/item.model.ts +++ b/src/app/models/item.model.ts @@ -5,7 +5,7 @@ export interface Photo { type?: string; } -export interface Callback { +export interface Review { rating?: number; content?: string; userID?: string; @@ -13,6 +13,9 @@ export interface Callback { timestamp?: string; } +/** @deprecated Use {@link Review} instead */ +export type Callback = Review; + export interface Question { question: string; answer: string; @@ -31,10 +34,9 @@ export interface Item { discount: number; remainings?: string; rating: number; - callbacks: Callback[] | null; + callbacks: Review[] | null; questions: Question[] | null; partnerID?: string; - quantity?: number; // For cart items } export interface CartItem extends Item { diff --git a/src/app/pages/cart/cart.component.ts b/src/app/pages/cart/cart.component.ts index 8cc3531..007b692 100644 --- a/src/app/pages/cart/cart.component.ts +++ b/src/app/pages/cart/cart.component.ts @@ -1,4 +1,4 @@ -import { Component, computed, ChangeDetectionStrategy, signal, OnDestroy, OnInit } from '@angular/core'; +import { Component, computed, ChangeDetectionStrategy, signal, OnDestroy } from '@angular/core'; import { DecimalPipe } from '@angular/common'; import { Router, RouterLink } from '@angular/router'; import { FormsModule } from '@angular/forms'; @@ -17,7 +17,7 @@ import { getDiscountedPrice, getMainImage, trackByItemId } from '../../utils/ite styleUrls: ['./cart.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class CartComponent implements OnInit, OnDestroy { +export class CartComponent implements OnDestroy { items; itemCount; totalPrice; @@ -59,10 +59,6 @@ export class CartComponent implements OnInit, OnDestroy { this.totalPrice = this.cartService.totalPrice; } - ngOnInit(): void { - // Component initialized - } - ngOnDestroy(): void { this.stopPolling(); if (this.closeTimeout) { @@ -109,16 +105,16 @@ export class CartComponent implements OnInit, OnDestroy { }; const cleanup = () => { - document.removeEventListener('touchmove', onMove as any); + document.removeEventListener('touchmove', onMove); document.removeEventListener('touchend', cleanup); }; - document.addEventListener('touchmove', onMove as any); + document.addEventListener('touchmove', onMove); document.addEventListener('touchend', cleanup); } clearCart(): void { - if (confirm('�� �������, ��� ������ �������� �������?')) { + if (confirm('Вы уверены, что хотите очистить корзину?')) { this.cartService.clearCart(); } } @@ -129,7 +125,7 @@ export class CartComponent implements OnInit, OnDestroy { checkout(): void { if (!this.termsAccepted) { - alert('����������, ������� ������� ��������, �������� �������� � �������� ��� ����������� ���������� ������.'); + alert('Пожалуйста, примите условия оферты, политику возврата и возврата для подтверждения оформления заказа.'); return; } this.openPaymentPopup(); @@ -251,7 +247,7 @@ export class CartComponent implements OnInit, OnDestroy { this.linkCopied.set(true); setTimeout(() => this.linkCopied.set(false), 2000); }).catch(err => { - console.error('������ �����������:', err); + console.error('Ошибка копирования:', err); }); } } @@ -316,7 +312,7 @@ export class CartComponent implements OnInit, OnDestroy { next: () => { this.emailSubmitting.set(false); // Show success message - alert('Email ������� ���������! ��������� ���� �����.'); + alert('Email успешно отправлен! Проверьте свою почту.'); // Close popup and redirect to home page setTimeout(() => { this.closePaymentPopup(); @@ -326,7 +322,7 @@ export class CartComponent implements OnInit, OnDestroy { error: (err) => { console.error('Error submitting email:', err); this.emailSubmitting.set(false); - alert('��������� ������ ��� �������� email. ����������, ���������� �����.'); + alert('Произошла ошибка при отправке email. Пожалуйста, попробуйте снова.'); } }); } @@ -387,11 +383,11 @@ export class CartComponent implements OnInit, OnDestroy { } if (digitsOnly.length === 0) { - this.phoneError.set('����� �������� ����������'); + this.phoneError.set('Номер телефона обязателен'); } else if (digitsOnly.length < 11) { - this.phoneError.set(`������� ��� ${11 - digitsOnly.length} ����`); + this.phoneError.set(`Введите ещё ${11 - digitsOnly.length} цифр`); } else if (digitsOnly.length > 11) { - this.phoneError.set('������� ����� ����'); + this.phoneError.set('Слишком много цифр'); } else { this.phoneError.set(''); } @@ -419,19 +415,19 @@ export class CartComponent implements OnInit, OnDestroy { } if (email.length === 0) { - this.emailError.set('Email ����������'); + this.emailError.set('Email обязателен'); } else if (email.length < 5) { - this.emailError.set('Email ������� �������� (������� 5 ��������)'); + this.emailError.set('Email слишком короткий (минимум 5 символов)'); } else if (email.length > 100) { - this.emailError.set('Email ������� ������� (�������� 100 ��������)'); + this.emailError.set('Email слишком длинный (максимум 100 символов)'); } else if (!email.includes('@')) { - this.emailError.set('Email ������ ��������� @'); + this.emailError.set('Email должен содержать @'); } else if (!email.includes('.')) { - this.emailError.set('Email ������ ��������� ����� (.com, .ru � �.�.)'); + this.emailError.set('Email должен содержать домен (.com, .ru и т.д.)'); } else { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { - this.emailError.set('������������ ������ email'); + this.emailError.set('Некорректный формат email'); } else { this.emailError.set(''); } diff --git a/src/app/pages/category/category.component.ts b/src/app/pages/category/category.component.ts index 0ff571c..f930780 100644 --- a/src/app/pages/category/category.component.ts +++ b/src/app/pages/category/category.component.ts @@ -24,6 +24,7 @@ export class CategoryComponent implements OnInit, OnDestroy { private readonly count = 20; private isLoadingMore = false; private routeSubscription?: Subscription; + private scrollTimeout?: ReturnType; constructor( private route: ActivatedRoute, @@ -41,6 +42,7 @@ export class CategoryComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.routeSubscription?.unsubscribe(); + if (this.scrollTimeout) clearTimeout(this.scrollTimeout); } resetAndLoad(): void { @@ -80,8 +82,6 @@ export class CategoryComponent implements OnInit, OnDestroy { }); } - private scrollTimeout: any; - @HostListener('window:scroll') onScroll(): void { if (this.scrollTimeout) clearTimeout(this.scrollTimeout); diff --git a/src/app/pages/category/subcategories.component.ts b/src/app/pages/category/subcategories.component.ts index dc3161a..77408c0 100644 --- a/src/app/pages/category/subcategories.component.ts +++ b/src/app/pages/category/subcategories.component.ts @@ -1,7 +1,8 @@ -import { Component, OnInit, signal, ChangeDetectionStrategy } from '@angular/core'; +import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy } from '@angular/core'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ApiService } from '../../services'; import { Category } from '../../models'; +import { Subscription } from 'rxjs'; @Component({ selector: 'app-subcategories', @@ -10,13 +11,15 @@ import { Category } from '../../models'; styleUrls: ['./subcategories.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class SubcategoriesComponent implements OnInit { +export class SubcategoriesComponent implements OnInit, OnDestroy { categories = signal([]); subcategories = signal([]); loading = signal(true); error = signal(null); parentName = signal(''); + private routeSubscription?: Subscription; + constructor( private route: ActivatedRoute, private router: Router, @@ -24,12 +27,16 @@ export class SubcategoriesComponent implements OnInit { ) {} ngOnInit(): void { - this.route.params.subscribe(params => { + this.routeSubscription = this.route.params.subscribe(params => { const id = parseInt(params['id'], 10); this.loadForParent(id); }); } + ngOnDestroy(): void { + this.routeSubscription?.unsubscribe(); + } + private loadForParent(parentID: number): void { this.loading.set(true); this.apiService.getCategories().subscribe({ diff --git a/src/app/pages/home/home.component.html b/src/app/pages/home/home.component.html index 071f4c8..23ed3dd 100644 --- a/src/app/pages/home/home.component.html +++ b/src/app/pages/home/home.component.html @@ -130,7 +130,7 @@ class="dexar-category-card" [class.dexar-category-card--wide]="isWideCategory(category.categoryID)">
- @if (isWideCategory(category.categoryID) && category.wideBanner && category.wideBanner !== true) { + @if (isWideCategory(category.categoryID) && category.wideBanner) { } @else if (category.icon) { diff --git a/src/app/pages/home/home.component.ts b/src/app/pages/home/home.component.ts index 61b83a6..45970be 100644 --- a/src/app/pages/home/home.component.ts +++ b/src/app/pages/home/home.component.ts @@ -87,12 +87,6 @@ export class HomeComponent implements OnInit { topLevel.forEach(cat => { if (!cat.wideBanner) return; - // API may send wideBanner as a boolean flag instead of a URL - if (cat.wideBanner === true) { - this.wideCategories.update(set => { const next = new Set(set); next.add(cat.categoryID); return next; }); - return; - } - const img = new Image(); img.onload = () => { const ratio = img.naturalWidth / img.naturalHeight; @@ -104,7 +98,7 @@ export class HomeComponent implements OnInit { }); } }; - img.src = cat.wideBanner as string; + img.src = cat.wideBanner; }); } diff --git a/src/app/pages/info/about/about.component.ts b/src/app/pages/info/about/about.component.ts index b0af9aa..f15d2e8 100644 --- a/src/app/pages/info/about/about.component.ts +++ b/src/app/pages/info/about/about.component.ts @@ -1,11 +1,12 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; import { environment } from '../../../../environments/environment'; @Component({ selector: 'app-about', imports: [], templateUrl: './about.component.html', - styleUrls: ['./about.component.scss'] + styleUrls: ['./about.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class AboutComponent { brandName = environment.brandName; diff --git a/src/app/pages/info/contacts/contacts.component.ts b/src/app/pages/info/contacts/contacts.component.ts index 54013e7..bcdca85 100644 --- a/src/app/pages/info/contacts/contacts.component.ts +++ b/src/app/pages/info/contacts/contacts.component.ts @@ -1,11 +1,12 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; import { environment } from '../../../../environments/environment'; @Component({ selector: 'app-contacts', imports: [], templateUrl: './contacts.component.html', - styleUrls: ['./contacts.component.scss'] + styleUrls: ['./contacts.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class ContactsComponent { brandName = environment.brandName; diff --git a/src/app/pages/info/delivery/delivery.component.ts b/src/app/pages/info/delivery/delivery.component.ts index d2e3bf3..b112d80 100644 --- a/src/app/pages/info/delivery/delivery.component.ts +++ b/src/app/pages/info/delivery/delivery.component.ts @@ -1,11 +1,12 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; import { environment } from '../../../../environments/environment'; @Component({ selector: 'app-delivery', imports: [], templateUrl: './delivery.component.html', - styleUrls: ['./delivery.component.scss'] + styleUrls: ['./delivery.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class DeliveryComponent { brandName = environment.brandName; diff --git a/src/app/pages/info/faq/faq.component.ts b/src/app/pages/info/faq/faq.component.ts index 7a05eb5..40b2445 100644 --- a/src/app/pages/info/faq/faq.component.ts +++ b/src/app/pages/info/faq/faq.component.ts @@ -1,11 +1,12 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; import { environment } from '../../../../environments/environment'; @Component({ selector: 'app-faq', imports: [], templateUrl: './faq.component.html', - styleUrls: ['./faq.component.scss'] + styleUrls: ['./faq.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class FaqComponent { brandName = environment.brandName; diff --git a/src/app/pages/info/guarantee/guarantee.component.ts b/src/app/pages/info/guarantee/guarantee.component.ts index 3ae2e97..90ffc36 100644 --- a/src/app/pages/info/guarantee/guarantee.component.ts +++ b/src/app/pages/info/guarantee/guarantee.component.ts @@ -1,11 +1,12 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; import { environment } from '../../../../environments/environment'; @Component({ selector: 'app-guarantee', imports: [], templateUrl: './guarantee.component.html', - styleUrls: ['./guarantee.component.scss'] + styleUrls: ['./guarantee.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class GuaranteeComponent { brandName = environment.brandName; diff --git a/src/app/pages/item-detail/item-detail.component.ts b/src/app/pages/item-detail/item-detail.component.ts index 3ed8b25..96cc070 100644 --- a/src/app/pages/item-detail/item-detail.component.ts +++ b/src/app/pages/item-detail/item-detail.component.ts @@ -1,12 +1,14 @@ -import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy } from '@angular/core'; +import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy, inject } from '@angular/core'; import { DecimalPipe } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { ActivatedRoute, RouterLink } from '@angular/router'; -import { ApiService, CartService, TelegramService } from '../../services'; +import { ApiService, CartService, TelegramService, SeoService } from '../../services'; import { Item } from '../../models'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { Subscription } from 'rxjs'; import { environment } from '../../../environments/environment'; +import { SecurityContext } from '@angular/core'; +import { getDiscountedPrice } from '../../utils/item.utils'; @Component({ selector: 'app-item-detail', @@ -31,6 +33,11 @@ export class ItemDetailComponent implements OnInit, OnDestroy { reviewSubmitStatus = signal<'idle' | 'loading' | 'success' | 'error'>('idle'); private routeSubscription?: Subscription; + private reviewResetTimeout?: ReturnType; + private reviewErrorTimeout?: ReturnType; + private reloadTimeout?: ReturnType; + + private seoService = inject(SeoService); constructor( private route: ActivatedRoute, @@ -49,6 +56,10 @@ export class ItemDetailComponent implements OnInit, OnDestroy { ngOnDestroy(): void { this.routeSubscription?.unsubscribe(); + if (this.reviewResetTimeout) clearTimeout(this.reviewResetTimeout); + if (this.reviewErrorTimeout) clearTimeout(this.reviewErrorTimeout); + if (this.reloadTimeout) clearTimeout(this.reloadTimeout); + this.seoService.resetToDefaults(); } loadItem(itemID: number): void { @@ -57,6 +68,7 @@ export class ItemDetailComponent implements OnInit, OnDestroy { this.apiService.getItem(itemID).subscribe({ next: (item) => { this.item.set(item); + this.seoService.setItemMeta(item); this.loading.set(false); }, error: (err) => { @@ -81,18 +93,18 @@ export class ItemDetailComponent implements OnInit, OnDestroy { getDiscountedPrice(): number { const currentItem = this.item(); if (!currentItem) return 0; - return currentItem.price * (1 - currentItem.discount / 100); + return getDiscountedPrice(currentItem); } getSafeHtml(html: string): SafeHtml { - return this.sanitizer.sanitize(1, html) || ''; + return this.sanitizer.sanitize(SecurityContext.HTML, html) || ''; } getRatingStars(rating: number): string { const fullStars = Math.floor(rating); const hasHalfStar = rating % 1 >= 0.5; - let stars = '?'.repeat(fullStars); - if (hasHalfStar) stars += '?'; + let stars = '★'.repeat(fullStars); + if (hasHalfStar) stars += '☆'; return stars; } @@ -102,10 +114,10 @@ export class ItemDetailComponent implements OnInit, OnDestroy { const diffMs = now.getTime() - date.getTime(); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); - if (diffDays === 0) return '�������'; - if (diffDays === 1) return '�����'; - if (diffDays < 7) return `${diffDays} ��. �����`; - if (diffDays < 30) return `${Math.floor(diffDays / 7)} ���. �����`; + if (diffDays === 0) return 'Сегодня'; + if (diffDays === 1) return 'Вчера'; + if (diffDays < 7) return `${diffDays} дн. назад`; + if (diffDays < 30) return `${Math.floor(diffDays / 7)} нед. назад`; return date.toLocaleDateString('ru-RU', { day: 'numeric', @@ -120,7 +132,7 @@ export class ItemDetailComponent implements OnInit, OnDestroy { getUserDisplayName(): string | null { if (!this.telegramService.isTelegramApp()) { - return '������������'; + return 'Пользователь'; } return this.telegramService.getDisplayName(); } @@ -149,13 +161,13 @@ export class ItemDetailComponent implements OnInit, OnDestroy { this.reviewSubmitStatus.set('success'); this.newReview = { rating: 0, comment: '', anonymous: false }; - // ������ ��������� ����� 3 ������� - setTimeout(() => { + // Сброс состояния через 3 секунды + this.reviewResetTimeout = setTimeout(() => { this.reviewSubmitStatus.set('idle'); }, 3000); - // ������������� ������ ������ ����� ��������� �������� - setTimeout(() => { + // Перезагрузить данные товара после отправки отзыва + this.reloadTimeout = setTimeout(() => { this.loadItem(currentItem.itemID); }, 500); }, @@ -163,8 +175,8 @@ export class ItemDetailComponent implements OnInit, OnDestroy { console.error('Error submitting review:', err); this.reviewSubmitStatus.set('error'); - // ������ ��������� �� ������ ����� 5 ������ - setTimeout(() => { + // Сброс состояния об ошибке через 5 секунд + this.reviewErrorTimeout = setTimeout(() => { this.reviewSubmitStatus.set('idle'); }, 5000); } diff --git a/src/app/pages/legal/company-details/company-details.component.ts b/src/app/pages/legal/company-details/company-details.component.ts index ce23538..e568fda 100644 --- a/src/app/pages/legal/company-details/company-details.component.ts +++ b/src/app/pages/legal/company-details/company-details.component.ts @@ -1,11 +1,12 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; import { environment } from '../../../../environments/environment'; @Component({ selector: 'app-company-details', imports: [], templateUrl: './company-details.component.html', - styleUrls: ['./company-details.component.scss'] + styleUrls: ['./company-details.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class CompanyDetailsComponent { brandName = environment.brandName; diff --git a/src/app/pages/legal/payment-terms/payment-terms.component.ts b/src/app/pages/legal/payment-terms/payment-terms.component.ts index aa841e5..a05f352 100644 --- a/src/app/pages/legal/payment-terms/payment-terms.component.ts +++ b/src/app/pages/legal/payment-terms/payment-terms.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; import { RouterLink } from '@angular/router'; import { environment } from '../../../../environments/environment'; @@ -6,7 +6,8 @@ import { environment } from '../../../../environments/environment'; selector: 'app-payment-terms', imports: [RouterLink], templateUrl: './payment-terms.component.html', - styleUrls: ['./payment-terms.component.scss'] + styleUrls: ['./payment-terms.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class PaymentTermsComponent { brandName = environment.brandName; diff --git a/src/app/pages/legal/privacy-policy/privacy-policy.component.ts b/src/app/pages/legal/privacy-policy/privacy-policy.component.ts index f0841ac..c495f5c 100644 --- a/src/app/pages/legal/privacy-policy/privacy-policy.component.ts +++ b/src/app/pages/legal/privacy-policy/privacy-policy.component.ts @@ -1,11 +1,12 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; import { environment } from '../../../../environments/environment'; @Component({ selector: 'app-privacy-policy', imports: [], templateUrl: './privacy-policy.component.html', - styleUrls: ['./privacy-policy.component.scss'] + styleUrls: ['./privacy-policy.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class PrivacyPolicyComponent { brandName = environment.brandName; diff --git a/src/app/pages/legal/public-offer/public-offer.component.ts b/src/app/pages/legal/public-offer/public-offer.component.ts index 71cb01c..17941db 100644 --- a/src/app/pages/legal/public-offer/public-offer.component.ts +++ b/src/app/pages/legal/public-offer/public-offer.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; import { RouterLink } from '@angular/router'; import { environment } from '../../../../environments/environment'; @@ -6,7 +6,8 @@ import { environment } from '../../../../environments/environment'; selector: 'app-public-offer', imports: [RouterLink], templateUrl: './public-offer.component.html', - styleUrls: ['./public-offer.component.scss'] + styleUrls: ['./public-offer.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class PublicOfferComponent { brandName = environment.brandName; diff --git a/src/app/pages/legal/return-policy/return-policy.component.ts b/src/app/pages/legal/return-policy/return-policy.component.ts index a8faa7e..8ebc21e 100644 --- a/src/app/pages/legal/return-policy/return-policy.component.ts +++ b/src/app/pages/legal/return-policy/return-policy.component.ts @@ -1,12 +1,12 @@ -import { Component } from '@angular/core'; -import { RouterLink } from '@angular/router'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; import { environment } from '../../../../environments/environment'; @Component({ selector: 'app-return-policy', imports: [], templateUrl: './return-policy.component.html', - styleUrls: ['./return-policy.component.scss'] + styleUrls: ['./return-policy.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class ReturnPolicyComponent { brandName = environment.brandName; diff --git a/src/app/pages/search/search.component.ts b/src/app/pages/search/search.component.ts index 570b61d..54e3ded 100644 --- a/src/app/pages/search/search.component.ts +++ b/src/app/pages/search/search.component.ts @@ -48,6 +48,7 @@ export class SearchComponent implements OnDestroy { ngOnDestroy(): void { this.searchSubscription.unsubscribe(); this.searchSubject.complete(); + if (this.scrollTimeout) clearTimeout(this.scrollTimeout); } onSearchInput(query: string): void { @@ -106,7 +107,7 @@ export class SearchComponent implements OnDestroy { }); } - private scrollTimeout: any; + private scrollTimeout?: ReturnType; @HostListener('window:scroll') onScroll(): void { diff --git a/src/app/services/cart.service.ts b/src/app/services/cart.service.ts index f430b64..ce8de1f 100644 --- a/src/app/services/cart.service.ts +++ b/src/app/services/cart.service.ts @@ -1,6 +1,7 @@ import { Injectable, signal, computed, effect } from '@angular/core'; import { ApiService } from './api.service'; import { Item, CartItem } from '../models'; +import { getDiscountedPrice } from '../utils/item.utils'; import { environment } from '../../environments/environment'; import type { } from '../types/telegram.types'; @@ -11,6 +12,7 @@ export class CartService { private readonly STORAGE_KEY = `${environment.brandName.toLowerCase().replace(/\s+/g, '_')}_cart`; private cartItems = signal([]); private isTelegram = typeof window !== 'undefined' && !!window.Telegram?.WebApp; + private addingItems = new Set(); items = this.cartItems.asReadonly(); itemCount = computed(() => { @@ -22,8 +24,7 @@ export class CartService { const items = this.cartItems(); if (!Array.isArray(items)) return 0; return items.reduce((total, item) => { - const price = item.price * (1 - item.discount / 100); - return total + (price * item.quantity); + return total + (getDiscountedPrice(item) * item.quantity); }, 0); }); @@ -40,8 +41,8 @@ export class CartService { private saveToStorage(items: CartItem[]): void { const data = JSON.stringify(items); - // Always save to sessionStorage - sessionStorage.setItem(this.STORAGE_KEY, data); + // Always save to localStorage + localStorage.setItem(this.STORAGE_KEY, data); // Also save to Telegram CloudStorage if available if (this.isTelegram) { @@ -59,21 +60,21 @@ export class CartService { window.Telegram!.WebApp.CloudStorage.getItem(this.STORAGE_KEY, (err, value) => { if (err) { console.error('Error loading from Telegram CloudStorage:', err); - this.loadFromSessionStorage(); + this.loadFromLocalStorage(); } else if (value) { - this.parseAndSetCart(value) || this.loadFromSessionStorage(); + this.parseAndSetCart(value) || this.loadFromLocalStorage(); } else { - // No data in CloudStorage, try sessionStorage - this.loadFromSessionStorage(); + // No data in CloudStorage, try localStorage + this.loadFromLocalStorage(); } }); } else { - this.loadFromSessionStorage(); + this.loadFromLocalStorage(); } } - private loadFromSessionStorage(): void { - const stored = sessionStorage.getItem(this.STORAGE_KEY); + private loadFromLocalStorage(): void { + const stored = localStorage.getItem(this.STORAGE_KEY); if (stored) { this.parseAndSetCart(stored); } @@ -98,6 +99,9 @@ export class CartService { } addItem(itemID: number, quantity: number = 1): void { + // Prevent duplicate API calls for same item + if (this.addingItems.has(itemID)) return; + const currentItems = this.cartItems(); const existingItem = currentItems.find(i => i.itemID === itemID); @@ -106,14 +110,16 @@ export class CartService { this.updateQuantity(itemID, existingItem.quantity + quantity); } else { // Get item details from API and add to cart + this.addingItems.add(itemID); this.apiService.getItem(itemID).subscribe({ next: (item) => { const cartItem: CartItem = { ...item, quantity }; this.cartItems.set([...this.cartItems(), cartItem]); + this.addingItems.delete(itemID); }, error: (err) => { console.error('Error adding to cart:', err); - alert('Ошибка добавления в корзину: ' + (err.error?.message || err.message)); + this.addingItems.delete(itemID); } }); } diff --git a/src/app/services/index.ts b/src/app/services/index.ts index 4247c75..4f32ba6 100644 --- a/src/app/services/index.ts +++ b/src/app/services/index.ts @@ -2,3 +2,4 @@ export * from './api.service'; export * from './cart.service'; export * from './telegram.service'; export * from './language.service'; +export * from './seo.service'; diff --git a/src/app/services/seo.service.ts b/src/app/services/seo.service.ts new file mode 100644 index 0000000..0e4673c --- /dev/null +++ b/src/app/services/seo.service.ts @@ -0,0 +1,117 @@ +import { Injectable, inject } from '@angular/core'; +import { Meta, Title } from '@angular/platform-browser'; +import { environment } from '../../environments/environment'; +import { Item } from '../models'; +import { getDiscountedPrice, getMainImage } from '../utils/item.utils'; + +@Injectable({ + providedIn: 'root' +}) +export class SeoService { + private meta = inject(Meta); + private title = inject(Title); + + private readonly siteUrl = `https://${environment.domain}`; + private readonly siteName = environment.brandFullName; + + /** + * Set Open Graph & Twitter Card meta tags for a product/item page. + */ + setItemMeta(item: Item): void { + const price = item.discount > 0 ? getDiscountedPrice(item) : item.price; + const imageUrl = this.resolveUrl(getMainImage(item)); + const itemUrl = `${this.siteUrl}/item/${item.itemID}`; + const description = this.truncate(this.stripHtml(item.description), 160); + const titleText = `${item.name} — ${this.siteName}`; + + this.title.setTitle(titleText); + + this.setOrUpdate([ + // Open Graph + { property: 'og:type', content: 'product' }, + { property: 'og:title', content: item.name }, + { property: 'og:description', content: description }, + { property: 'og:image', content: imageUrl }, + { property: 'og:url', content: itemUrl }, + { property: 'og:site_name', content: this.siteName }, + { property: 'og:locale', content: 'ru_RU' }, + + // Product-specific OG tags + { property: 'product:price:amount', content: price.toFixed(2) }, + { property: 'product:price:currency', content: item.currency || 'RUB' }, + + // Twitter Card + { name: 'twitter:card', content: 'summary_large_image' }, + { name: 'twitter:title', content: item.name }, + { name: 'twitter:description', content: description }, + { name: 'twitter:image', content: imageUrl }, + + // Standard meta + { name: 'description', content: description }, + ]); + } + + /** + * Reset meta tags back to defaults (call on navigation away from item page). + */ + resetToDefaults(): void { + const defaultTitle = `${this.siteName} — Маркетплейс товаров и услуг`; + const defaultDescription = 'Современный маркетплейс для покупки цифровых товаров. Широкий выбор товаров, удобный поиск, быстрая доставка.'; + const defaultImage = `${this.siteUrl}/og-image.jpg`; + + this.title.setTitle(defaultTitle); + + this.setOrUpdate([ + { property: 'og:type', content: 'website' }, + { property: 'og:title', content: defaultTitle }, + { property: 'og:description', content: defaultDescription }, + { property: 'og:image', content: defaultImage }, + { property: 'og:url', content: this.siteUrl }, + { property: 'og:site_name', content: this.siteName }, + { property: 'og:locale', content: 'ru_RU' }, + + { name: 'twitter:card', content: 'summary_large_image' }, + { name: 'twitter:title', content: defaultTitle }, + { name: 'twitter:description', content: defaultDescription }, + { name: 'twitter:image', content: defaultImage }, + + { name: 'description', content: defaultDescription }, + ]); + + // Remove product-specific tags + this.meta.removeTag("property='product:price:amount'"); + this.meta.removeTag("property='product:price:currency'"); + } + + private setOrUpdate(tags: Array<{ property?: string; name?: string; content: string }>): void { + for (const tag of tags) { + const selector = tag.property + ? `property='${tag.property}'` + : `name='${tag.name}'`; + + const existing = this.meta.getTag(selector); + if (existing) { + this.meta.updateTag(tag as any, selector); + } else { + this.meta.addTag(tag as any); + } + } + } + + /** Convert relative URLs to absolute */ + private resolveUrl(url: string): string { + if (!url || url.startsWith('http')) return url; + return `${this.siteUrl}${url.startsWith('/') ? '' : '/'}${url}`; + } + + /** Strip HTML tags from a string */ + private stripHtml(html: string): string { + return html?.replace(/<[^>]*>/g, '') || ''; + } + + /** Truncate text to maxLength, adding ellipsis */ + private truncate(text: string, maxLength: number): string { + if (!text || text.length <= maxLength) return text || ''; + return text.substring(0, maxLength - 1) + '…'; + } +} diff --git a/src/app/utils/item.utils.ts b/src/app/utils/item.utils.ts index dcd7809..04c6ccf 100644 --- a/src/app/utils/item.utils.ts +++ b/src/app/utils/item.utils.ts @@ -5,7 +5,7 @@ export function getDiscountedPrice(item: Item): number { } export function getMainImage(item: Item): string { - return item.photos?.[0]?.url || ''; + return item.photos?.[0]?.url || '/assets/images/placeholder.svg'; } export function trackByItemId(index: number, item: Item): number { diff --git a/src/index.html b/src/index.html index bdf99dc..b8204c5 100644 --- a/src/index.html +++ b/src/index.html @@ -49,8 +49,11 @@ - - + + + + +