diff --git a/src/app/app.config.ts b/src/app/app.config.ts index b9aaf50..35bef9f 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,5 +1,5 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection, isDevMode } from '@angular/core'; -import { PreloadAllModules, provideRouter, withPreloading, withInMemoryScrolling } from '@angular/router'; +import { provideRouter, withInMemoryScrolling } from '@angular/router'; import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { routes } from './app.routes'; @@ -11,8 +11,7 @@ export const appConfig: ApplicationConfig = { provideBrowserGlobalErrorListeners(), provideZoneChangeDetection({ eventCoalescing: true }), provideRouter( - routes, - withPreloading(PreloadAllModules), + routes, withInMemoryScrolling({ scrollPositionRestoration: 'top' }) ), provideHttpClient( diff --git a/src/app/app.html b/src/app/app.html index 3b4e3f4..ee0a585 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -1,20 +1,16 @@ @if (checkingServer()) {
-
-
-

Проверка соединения с сервером...

-
+
+

Подключение к серверу...

} @else if (!serverAvailable()) {
-
-
⚠️
-

Извините, возникла проблема

-

Не удается подключиться к серверу. Пожалуйста, проверьте подключение к интернету или попробуйте позже.

- -
+
⚠️
+

Сервер недоступен

+

Не удалось подключиться к серверу. Проверьте подключение к интернету.

+
-} @else { +} @else { @if (!isHomePage()) { diff --git a/src/app/app.scss b/src/app/app.scss index 3a7a0a2..31f32ec 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -7,81 +7,58 @@ .server-check-overlay, .server-error-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: #f8f9fa; display: flex; + flex-direction: column; align-items: center; justify-content: center; - z-index: 9999; -} - -.server-check-content, -.server-error-content { + min-height: 100vh; text-align: center; - padding: 40px; - max-width: 500px; + padding: 2rem; + background: var(--surface-ground, #f8f9fa); + color: var(--text-color, #333); } .spinner-large { - width: 60px; - height: 60px; - border: 6px solid #f3f3f3; - border-top: 6px solid var(--primary-color); + width: 48px; + height: 48px; + border: 4px solid var(--surface-border, #dee2e6); + border-top-color: var(--primary-color, #007bff); border-radius: 50%; - animation: spin 1s linear infinite; - margin: 0 auto 24px; + animation: spin 0.8s linear infinite; + margin-bottom: 1rem; } @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -.server-check-content h2 { - color: #333; - font-size: 1.5rem; - margin: 0; + to { transform: rotate(360deg); } } .error-icon { - font-size: 5rem; - margin-bottom: 20px; + font-size: 3rem; + margin-bottom: 1rem; } -.server-error-content h1 { - font-size: 2rem; - color: #333; - margin: 0 0 16px 0; +.server-error-overlay h2 { + margin: 0 0 0.5rem; + font-size: 1.25rem; } -.server-error-content p { - font-size: 1.1rem; - color: #333; - line-height: 1.6; - margin: 0 0 32px 0; +.server-error-overlay p { + margin: 0 0 1.5rem; + opacity: 0.7; + max-width: 300px; } .retry-btn { - padding: 14px 32px; - background: var(--primary-color); - color: white; + padding: 0.75rem 2rem; border: none; border-radius: 8px; - font-size: 1.1rem; - font-weight: 600; + background: var(--primary-color, #007bff); + color: #fff; + font-size: 1rem; cursor: pointer; - transition: background 0.2s; + transition: opacity 0.2s; &:hover { - background: var(--primary-hover); - } - - - &:active { - transform: scale(0.98); + opacity: 0.85; } } diff --git a/src/app/app.ts b/src/app/app.ts index e5fdde7..9563e2f 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,106 +1,98 @@ -import { Component, OnInit, OnDestroy, signal, ApplicationRef } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { Component, OnInit, signal, ApplicationRef, inject, DestroyRef } from '@angular/core'; import { Router, RouterOutlet, NavigationEnd } from '@angular/router'; import { Title } from '@angular/platform-browser'; import { HeaderComponent } from './components/header/header.component'; import { FooterComponent } from './components/footer/footer.component'; import { BackButtonComponent } from './components/back-button/back-button.component'; import { ApiService } from './services'; -import { Subscription, interval, concat } from 'rxjs'; +import { interval, concat } from 'rxjs'; import { filter, first } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { environment } from '../environments/environment'; import { SwUpdate } from '@angular/service-worker'; @Component({ selector: 'app-root', - imports: [RouterOutlet, HeaderComponent, FooterComponent, BackButtonComponent, CommonModule], + imports: [RouterOutlet, HeaderComponent, FooterComponent, BackButtonComponent], templateUrl: './app.html', styleUrl: './app.scss' }) -export class App implements OnInit, OnDestroy { +export class App implements OnInit { protected title = environment.brandName; - serverAvailable = signal(true); - checkingServer = signal(true); isHomePage = signal(true); - private pingSubscription?: Subscription; - private updateSubscription?: Subscription; - private routerSubscription?: Subscription; + checkingServer = signal(true); + serverAvailable = signal(false); - constructor( - private apiService: ApiService, - private titleService: Title, - private swUpdate: SwUpdate, - private appRef: ApplicationRef, - private router: Router - ) {} + private destroyRef = inject(DestroyRef); + private apiService = inject(ApiService); + private titleService = inject(Title); + private swUpdate = inject(SwUpdate); + private appRef = inject(ApplicationRef); + private router = inject(Router); ngOnInit(): void { - // Устанавливаем заголовок страницы в зависимости от бренда this.titleService.setTitle(`${environment.brandFullName} - Маркетплейс товаров и услуг`); this.checkServerHealth(); this.setupAutoUpdates(); // Track route changes to show/hide back button - this.routerSubscription = this.router.events - .pipe(filter(event => event instanceof NavigationEnd)) + this.router.events + .pipe( + filter(event => event instanceof NavigationEnd), + takeUntilDestroyed(this.destroyRef) + ) .subscribe((event) => { const url = (event as NavigationEnd).urlAfterRedirects || (event as NavigationEnd).url; this.isHomePage.set(url === '/' || url === '/home' || url === ''); }); } - checkServerHealth(): void { - this.pingSubscription = this.apiService.ping().subscribe({ - next: (response) => { - // Server is available - this.serverAvailable.set(true); - this.checkingServer.set(false); - }, - error: (err) => { - console.error('Server health check failed:', err); - // Allow app to continue even if server is unreachable - this.serverAvailable.set(true); - this.checkingServer.set(false); - } - }); + private checkServerHealth(): void { + this.checkingServer.set(true); + this.apiService.ping() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: () => { + this.serverAvailable.set(true); + this.checkingServer.set(false); + }, + error: () => { + this.serverAvailable.set(false); + this.checkingServer.set(false); + } + }); } - setupAutoUpdates(): void { + retryConnection(): void { + this.checkServerHealth(); + } + + private setupAutoUpdates(): void { if (!this.swUpdate.isEnabled) { return; } - // Check for updates every 6 hours const appIsStable$ = this.appRef.isStable.pipe(first(isStable => isStable === true)); const every6Hours$ = interval(6 * 60 * 60 * 1000); const checkInterval$ = concat(appIsStable$, every6Hours$); - this.updateSubscription = checkInterval$.subscribe(async () => { - try { - await this.swUpdate.checkForUpdate(); - } catch (err) { - console.error('Update check failed:', err); - } - }); + checkInterval$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(async () => { + try { + await this.swUpdate.checkForUpdate(); + } catch (err) { + console.error('Update check failed:', err); + } + }); - // Silently activate updates when ready - this.swUpdate.versionUpdates.subscribe(event => { - if (event.type === 'VERSION_READY') { - // Update will activate on next navigation/reload automatically - console.log('New app version ready'); - } - }); - } - - ngOnDestroy(): void { - this.pingSubscription?.unsubscribe(); - this.updateSubscription?.unsubscribe(); - this.routerSubscription?.unsubscribe(); - } - - retryConnection(): void { - this.checkingServer.set(true); - this.checkServerHealth(); + this.swUpdate.versionUpdates + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(event => { + if (event.type === 'VERSION_READY') { + console.log('New app version ready'); + } + }); } } 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 fc1633f..858280e 100644 --- a/src/app/brands/novo/pages/info/about/about.component.ts +++ b/src/app/brands/novo/pages/info/about/about.component.ts @@ -1,10 +1,8 @@ import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; @Component({ selector: 'app-about-novo', - standalone: true, - imports: [CommonModule], + imports: [], templateUrl: './about.component.html', styleUrls: ['../../../../../pages/info/about/about.component.scss'] }) 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 91c572e..c87b3ef 100644 --- a/src/app/brands/novo/pages/info/contacts/contacts.component.ts +++ b/src/app/brands/novo/pages/info/contacts/contacts.component.ts @@ -1,10 +1,8 @@ import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; @Component({ selector: 'app-contacts-novo', - standalone: true, - imports: [CommonModule], + imports: [], templateUrl: './contacts.component.html', styleUrls: ['../../../../../pages/info/contacts/contacts.component.scss'] }) 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 c23b734..971a09e 100644 --- a/src/app/brands/novo/pages/info/delivery/delivery.component.ts +++ b/src/app/brands/novo/pages/info/delivery/delivery.component.ts @@ -1,10 +1,8 @@ import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; @Component({ selector: 'app-delivery-novo', - standalone: true, - imports: [CommonModule], + imports: [], templateUrl: './delivery.component.html', styleUrls: ['../../../../../pages/info/delivery/delivery.component.scss'] }) 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 849e2c5..9294424 100644 --- a/src/app/brands/novo/pages/info/faq/faq.component.ts +++ b/src/app/brands/novo/pages/info/faq/faq.component.ts @@ -1,10 +1,8 @@ import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; @Component({ selector: 'app-faq-novo', - standalone: true, - imports: [CommonModule], + imports: [], templateUrl: './faq.component.html', styleUrls: ['../../../../../pages/info/faq/faq.component.scss'] }) 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 891e764..08877bb 100644 --- a/src/app/brands/novo/pages/info/guarantee/guarantee.component.ts +++ b/src/app/brands/novo/pages/info/guarantee/guarantee.component.ts @@ -1,10 +1,8 @@ import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; @Component({ selector: 'app-guarantee-novo', - standalone: true, - imports: [CommonModule], + imports: [], templateUrl: './guarantee.component.html', styleUrls: ['../../../../../pages/info/guarantee/guarantee.component.scss'] }) 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 b93417c..79b17e9 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,10 +1,8 @@ import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; @Component({ selector: 'app-company-details-novo', - standalone: true, - imports: [CommonModule], + imports: [], templateUrl: './company-details.component.html', styleUrls: ['../../../../../pages/legal/company-details/company-details.component.scss'] }) 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 b6194ca..995ccb0 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,10 +1,8 @@ import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; @Component({ selector: 'app-payment-terms-novo', - standalone: true, - imports: [CommonModule], + imports: [], templateUrl: './payment-terms.component.html', styleUrls: ['../../../../../pages/legal/payment-terms/payment-terms.component.scss'] }) 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 b1e74b8..de07fe2 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,10 +1,8 @@ import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; @Component({ selector: 'app-privacy-policy-novo', - standalone: true, - imports: [CommonModule], + imports: [], templateUrl: './privacy-policy.component.html', styleUrls: ['../../../../../pages/legal/privacy-policy/privacy-policy.component.scss'] }) 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 2af0725..853e80a 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,10 +1,8 @@ import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; @Component({ selector: 'app-public-offer-novo', - standalone: true, - imports: [CommonModule], + imports: [], templateUrl: './public-offer.component.html', styleUrls: ['../../../../../pages/legal/public-offer/public-offer.component.scss'] }) 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 5680bbe..a9b3213 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,10 +1,8 @@ import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; @Component({ selector: 'app-return-policy-novo', - standalone: true, - imports: [CommonModule], + imports: [], templateUrl: './return-policy.component.html', styleUrls: ['../../../../../pages/legal/return-policy/return-policy.component.scss'] }) diff --git a/src/app/components/back-button/back-button.component.ts b/src/app/components/back-button/back-button.component.ts index ba2dbb0..217a8df 100644 --- a/src/app/components/back-button/back-button.component.ts +++ b/src/app/components/back-button/back-button.component.ts @@ -4,7 +4,6 @@ import { environment } from '../../../environments/environment'; @Component({ selector: 'app-back-button', - standalone: true, template: ` @if (!isnovo) { } diff --git a/src/app/components/items-carousel/items-carousel.component.ts b/src/app/components/items-carousel/items-carousel.component.ts index 51eb407..6773297 100644 --- a/src/app/components/items-carousel/items-carousel.component.ts +++ b/src/app/components/items-carousel/items-carousel.component.ts @@ -1,5 +1,5 @@ -import { Component, OnInit, signal } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { Component, OnInit, signal, ChangeDetectionStrategy } from '@angular/core'; +import { DecimalPipe } from '@angular/common'; import { RouterLink } from '@angular/router'; import { CarouselModule } from 'primeng/carousel'; import { ButtonModule } from 'primeng/button'; @@ -7,13 +7,14 @@ import { TagModule } from 'primeng/tag'; import { ApiService, CartService } from '../../services'; import { Item } from '../../models'; import { environment } from '../../../environments/environment'; +import { getDiscountedPrice, getMainImage } from '../../utils/item.utils'; @Component({ selector: 'app-items-carousel', templateUrl: './items-carousel.component.html', - standalone: true, - imports: [CommonModule, RouterLink, CarouselModule, ButtonModule, TagModule], - styleUrls: ['./items-carousel.component.scss'] + imports: [DecimalPipe, RouterLink, CarouselModule, ButtonModule, TagModule], + styleUrls: ['./items-carousel.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class ItemsCarouselComponent implements OnInit { products = signal([]); @@ -93,19 +94,8 @@ export class ItemsCarouselComponent implements OnInit { } } - getItemImage(item: Item): string { - if (item.photos && item.photos.length > 0 && item.photos[0]?.url) { - return item.photos[0].url; - } - return '/assets/images/placeholder.jpg'; - } - - getDiscountedPrice(item: Item): number { - if (item.discount > 0) { - return item.price * (1 - item.discount / 100); - } - return item.price; - } + readonly getItemImage = getMainImage; + readonly getDiscountedPrice = getDiscountedPrice; addToCart(event: Event, item: Item): void { event.preventDefault(); diff --git a/src/app/components/language-selector/language-selector.component.ts b/src/app/components/language-selector/language-selector.component.ts index 44b535b..7debf86 100644 --- a/src/app/components/language-selector/language-selector.component.ts +++ b/src/app/components/language-selector/language-selector.component.ts @@ -1,13 +1,12 @@ -import { Component, HostListener, ElementRef } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { Component, HostListener, ElementRef, ChangeDetectionStrategy } from '@angular/core'; import { LanguageService, Language } from '../../services/language.service'; @Component({ selector: 'app-language-selector', - standalone: true, - imports: [CommonModule], + imports: [], templateUrl: './language-selector.component.html', - styleUrls: ['./language-selector.component.scss'] + styleUrls: ['./language-selector.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush }) export class LanguageSelectorComponent { dropdownOpen = false; diff --git a/src/app/components/logo/logo.component.ts b/src/app/components/logo/logo.component.ts index 02ae181..18afc32 100644 --- a/src/app/components/logo/logo.component.ts +++ b/src/app/components/logo/logo.component.ts @@ -3,7 +3,6 @@ import { environment } from '../../../environments/environment'; @Component({ selector: 'app-logo', - standalone: true, template: ``, styles: [` .logo-img { diff --git a/src/app/pages/cart/cart.component.ts b/src/app/pages/cart/cart.component.ts index bc24c32..8cc3531 100644 --- a/src/app/pages/cart/cart.component.ts +++ b/src/app/pages/cart/cart.component.ts @@ -1,5 +1,5 @@ -import { Component, computed, ChangeDetectionStrategy, signal, OnDestroy, OnInit } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { Component, computed, ChangeDetectionStrategy, signal, OnDestroy, OnInit } from '@angular/core'; +import { DecimalPipe } from '@angular/common'; import { Router, RouterLink } from '@angular/router'; import { FormsModule } from '@angular/forms'; import { CartService, ApiService } from '../../services'; @@ -8,11 +8,11 @@ import { interval, Subscription } from 'rxjs'; import { switchMap, take } from 'rxjs/operators'; import { EmptyCartIconComponent } from '../../components/empty-cart-icon/empty-cart-icon.component'; import { environment } from '../../../environments/environment'; +import { getDiscountedPrice, getMainImage, trackByItemId } from '../../utils/item.utils'; @Component({ selector: 'app-cart', - standalone: true, - imports: [CommonModule, RouterLink, FormsModule, EmptyCartIconComponent], + imports: [DecimalPipe, RouterLink, FormsModule, EmptyCartIconComponent], templateUrl: './cart.component.html', styleUrls: ['./cart.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush @@ -118,27 +118,18 @@ export class CartComponent implements OnInit, OnDestroy { } clearCart(): void { - if (confirm('Вы уверены, что хотите очистить корзину?')) { + if (confirm('�� �������, ��� ������ �������� �������?')) { this.cartService.clearCart(); } } - getMainImage(item: Item): string { - return item.photos?.[0]?.url || ''; - } - - // TrackBy function for performance optimization - trackByItemId(index: number, item: Item): number { - return item.itemID; - } - - getDiscountedPrice(item: Item): number { - return item.price * (1 - item.discount / 100); - } + readonly getMainImage = getMainImage; + readonly trackByItemId = trackByItemId; + readonly getDiscountedPrice = getDiscountedPrice; checkout(): void { if (!this.termsAccepted) { - alert('Пожалуйста, примите условия договора, политику возврата и гарантии для продолжения оформления заказа.'); + alert('����������, ������� ������� ��������, �������� �������� � �������� ��� ����������� ���������� ������.'); return; } this.openPaymentPopup(); @@ -260,7 +251,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); }); } } @@ -325,7 +316,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(); @@ -335,7 +326,7 @@ export class CartComponent implements OnInit, OnDestroy { error: (err) => { console.error('Error submitting email:', err); this.emailSubmitting.set(false); - alert('Произошла ошибка при отправке email. Пожалуйста, попробуйте снова.'); + alert('��������� ������ ��� �������� email. ����������, ���������� �����.'); } }); } @@ -396,11 +387,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(''); } @@ -428,19 +419,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 0c009ef..0ff571c 100644 --- a/src/app/pages/category/category.component.ts +++ b/src/app/pages/category/category.component.ts @@ -1,14 +1,14 @@ import { Component, OnInit, OnDestroy, signal, HostListener, ChangeDetectionStrategy } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { DecimalPipe } from '@angular/common'; import { ActivatedRoute, RouterLink } from '@angular/router'; import { ApiService, CartService } from '../../services'; import { Item } from '../../models'; import { Subscription } from 'rxjs'; +import { getDiscountedPrice, getMainImage, trackByItemId } from '../../utils/item.utils'; @Component({ selector: 'app-category', - standalone: true, - imports: [CommonModule, RouterLink], + imports: [DecimalPipe, RouterLink], templateUrl: './category.component.html', styleUrls: ['./category.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush @@ -102,16 +102,7 @@ export class CategoryComponent implements OnInit, OnDestroy { this.cartService.addItem(itemID); } - getDiscountedPrice(item: Item): number { - return item.price * (1 - item.discount / 100); - } - - getMainImage(item: Item): string { - return item.photos?.[0]?.url || ''; - } - - // TrackBy function for performance optimization - trackByItemId(index: number, item: Item): number { - return item.itemID; - } + readonly getDiscountedPrice = getDiscountedPrice; + readonly getMainImage = getMainImage; + readonly trackByItemId = trackByItemId; } diff --git a/src/app/pages/category/subcategories.component.ts b/src/app/pages/category/subcategories.component.ts index 8e0780a..dc3161a 100644 --- a/src/app/pages/category/subcategories.component.ts +++ b/src/app/pages/category/subcategories.component.ts @@ -1,13 +1,11 @@ import { Component, OnInit, signal, ChangeDetectionStrategy } from '@angular/core'; -import { CommonModule } from '@angular/common'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ApiService } from '../../services'; import { Category } from '../../models'; @Component({ selector: 'app-subcategories', - standalone: true, - imports: [CommonModule, RouterLink], + imports: [RouterLink], templateUrl: './subcategories.component.html', styleUrls: ['./subcategories.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush diff --git a/src/app/pages/home/home.component.html b/src/app/pages/home/home.component.html index a47824a..23ed3dd 100644 --- a/src/app/pages/home/home.component.html +++ b/src/app/pages/home/home.component.html @@ -41,7 +41,7 @@

Выберите интересующую категорию

- @if (getTopLevelCategories().length === 0) { + @if (topLevelCategories().length === 0) {
📦

Категории скоро появятся

@@ -49,7 +49,7 @@
} @else {
- @for (category of getTopLevelCategories(); track category.categoryID) { + @for (category of topLevelCategories(); track category.categoryID) {
@if (category.icon) { @@ -117,7 +117,7 @@ @if (!loading() && !error()) {

Каталог товаров

- @if (getTopLevelCategories().length === 0) { + @if (topLevelCategories().length === 0) {
📦

Категории пока отсутствуют

@@ -125,7 +125,7 @@
} @else {
- @for (category of getTopLevelCategories(); track category.categoryID) { + @for (category of topLevelCategories(); track category.categoryID) { diff --git a/src/app/pages/home/home.component.scss b/src/app/pages/home/home.component.scss index 9ba10a5..30e555d 100644 --- a/src/app/pages/home/home.component.scss +++ b/src/app/pages/home/home.component.scss @@ -1,329 +1,25 @@ -.home-container { - max-width: 1200px; - margin: 0 auto; - padding: 20px; - animation: fadeIn 0.5s ease-in; -} - +// ========== SHARED ANIMATIONS ========== @keyframes fadeIn { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } } -.hero { - text-align: center; - padding: 80px 20px; - background: var(--gradient-hero); - color: white; - border-radius: var(--radius-xl); - margin-bottom: 50px; - position: relative; - overflow: hidden; - box-shadow: var(--shadow-lg); - - &.hero-compact { - padding: 35px 20px; - margin-bottom: 25px; - - h1 { - font-size: 2.2rem; - margin-bottom: 8px; - } - - p { - font-size: 1.1rem; - } - } - - &::before { - content: ''; - position: absolute; - top: -50%; - left: -50%; - width: 200%; - height: 200%; - background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%); - animation: pulse 4s ease-in-out infinite; - } - - @keyframes pulse { - 0%, 100% { - transform: scale(1); - opacity: 0.5; - } - 50% { - transform: scale(1.1); - opacity: 0.8; - } - } - - h1 { - font-size: 3.5rem; - margin: 0 0 15px 0; - font-weight: 700; - position: relative; - z-index: 1; - text-shadow: 0 2px 10px rgba(0,0,0,0.2); - animation: slideDown 0.8s ease-out; - } - - @keyframes slideDown { - from { - opacity: 0; - transform: translateY(-30px); - } - to { - opacity: 1; - transform: translateY(0); - } - } - - p { - font-size: 1.4rem; - margin: 0; - opacity: 0.95; - position: relative; - z-index: 1; - animation: slideUp 0.8s ease-out 0.2s both; - } - - @keyframes slideUp { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 0.95; - transform: translateY(0); - } - } +@keyframes fadeInUp { + from { opacity: 0; transform: translateY(30px); } + to { opacity: 1; transform: translateY(0); } } -.loading, -.error { - text-align: center; - padding: 60px 20px; -} - -.spinner { - width: 50px; - height: 50px; - border: 4px solid #f3f3f3; - border-top: 4px solid var(--primary-color); - border-radius: 50%; - animation: spin 1s linear infinite; - margin: 0 auto 20px; +@keyframes slideDown { + from { opacity: 0; transform: translateY(-30px); } + to { opacity: 1; transform: translateY(0); } } @keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } } -.error { - button { - margin-top: 20px; - padding: 10px 24px; - background: var(--primary-color); - color: white; - border: none; - border-radius: 6px; - cursor: pointer; - font-size: 1rem; - - &:hover { - background: var(--primary-hover); - } - } -} - -.categories { - h2 { - font-size: 2rem; - margin-bottom: 30px; - color: #333; - } -} - -.empty-categories { - text-align: center; - padding: 60px 20px; - background: white; - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - - .empty-icon { - font-size: 4rem; - margin-bottom: 20px; - opacity: 0.5; - } - - h3 { - font-size: 1.5rem; - color: #333; - margin: 0 0 10px 0; - } - - p { - color: #666; - font-size: 1rem; - margin: 0; - } -} - -.categories-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 30px; - animation: fadeIn 0.6s ease-in 0.3s both; -} - -.category-card { - background: white; - border-radius: var(--radius-lg); - box-shadow: var(--shadow-md); - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - height: 100%; - display: flex; - flex-direction: column; - overflow: hidden; - position: relative; - - &::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: var(--gradient-primary); - opacity: 0; - transition: opacity 0.3s ease; - z-index: 1; - } - - &:hover { - transform: translateY(-8px) scale(1.02); - box-shadow: var(--shadow-lg); - - &::before { - opacity: 0.1; - } - - .category-media img { - transform: scale(1.1); - } - - h3 { - color: var(--primary-color); - } - } -} - -.category-link { - display: flex; - flex-direction: column; - flex: 1; - text-decoration: none; - color: inherit; - position: relative; - min-height: 220px; - z-index: 2; - - .category-media { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: flex; - align-items: center; - justify-content: center; - overflow: hidden; - background: linear-gradient(135deg, #f6f7fb 0%, #e9ecf5 100%); - } - - .category-media img { - width: 100%; - height: 100%; - object-fit: contain; - background: white; - padding: 15px; - transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1), filter 0.5s ease; - filter: drop-shadow(0 4px 8px rgba(0,0,0,0.08)); - border-radius: 8px; - } - - &:hover .category-media img { - transform: scale(1.05); - filter: drop-shadow(0 16px 32px rgba(0,0,0,0.18)) saturate(1.1); - } - - .category-fallback { - font-size: 1.5rem; - font-weight: 700; - color: var(--primary-color); - background: var(--gradient-primary); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - text-align: center; - padding: 20px; - } - - h3 { - position: absolute; - bottom: 0; - left: 0; - right: 0; - margin: 0; - padding: 20px; - font-size: 1.3rem; - font-weight: 600; - color: #333; - background: linear-gradient(to top, rgba(255,255,255,0.98) 0%, rgba(255,255,255,0.95) 70%, transparent 100%); - z-index: 3; - transition: color 0.3s ease; - } -} - -@media (max-width: 768px) { - .home-container { - padding: 15px; - } - - .hero { - padding: 50px 20px; - border-radius: 15px; - - h1 { - font-size: 2.5rem; - } - - p { - font-size: 1.1rem; - } - } - - .categories-grid { - grid-template-columns: 1fr; - gap: 20px; - } - - .category-card { - &:hover { - transform: translateY(-4px) scale(1); - } - } -} - -// ========== novo HOME PAGE STYLES ========== +// ========== NOVO HOME PAGE STYLES ========== .novo-home { min-height: calc(100vh - 200px); animation: fadeIn 0.6s ease; @@ -619,26 +315,6 @@ } } -@keyframes slideDown { - from { - opacity: 0; - transform: translateY(-30px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - @media (max-width: 968px) { .novo-hero { padding: 4rem 2rem; @@ -747,42 +423,28 @@ animation: fadeInUp 0.8s ease-out; } -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(30px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - .dexar-hero-title { - font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-weight: 500; font-size: 42px; - color: #1e3c38; + color: var(--text-primary); line-height: 1.2; margin: 0; animation: fadeInUp 0.8s ease-out 0.1s both; } .dexar-hero-subtitle { - font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-weight: 500; font-size: 24px; - color: #1e3c38; + color: var(--text-primary); line-height: 1.3; margin: 0; animation: fadeInUp 0.8s ease-out 0.2s both; } .dexar-hero-tagline { - font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-weight: 500; font-size: 24px; - color: #1e3c38; + color: var(--text-primary); line-height: 1.3; margin: 0; animation: fadeInUp 0.8s ease-out 0.3s both; @@ -801,10 +463,9 @@ justify-content: center; width: 280px; height: 48px; - background: linear-gradient(360deg, #497671 0%, #a7ceca 100%); - border: 1px solid #d3dad9; - border-radius: 13px; - font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background: var(--gradient-primary); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); font-weight: 500; font-size: 20px; color: #ffffff; @@ -832,13 +493,12 @@ gap: 9px; width: 220px; height: 48px; - background: #f5f5f5; - border: 1px solid #d3dad9; - border-radius: 13px; - font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); font-weight: 500; font-size: 20px; - color: #1e3c38; + color: var(--text-primary); letter-spacing: 1.08px; cursor: pointer; transition: all 0.3s ease; @@ -879,7 +539,7 @@ width: 50px; height: 50px; border: 4px solid #f3f3f3; - border-top: 4px solid #497671; + border-top: 4px solid var(--primary-color); border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 20px; @@ -889,17 +549,17 @@ button { margin-top: 20px; padding: 12px 28px; - background: #497671; + background: var(--primary-color); color: white; border: none; - border-radius: 13px; + border-radius: var(--radius-lg); cursor: pointer; font-size: 1.1rem; font-weight: 500; transition: all 0.3s ease; &:hover { - background: #3d635f; + background: var(--primary-hover); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(73, 118, 113, 0.3); } @@ -914,11 +574,10 @@ } .dexar-categories-title { - font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 2.5rem; font-weight: 600; margin-bottom: 40px; - color: #1e3c38; + color: var(--text-primary); } .dexar-empty-categories { @@ -936,13 +595,13 @@ h3 { font-size: 1.8rem; - color: #1e3c38; + color: var(--text-primary); margin: 0 0 12px 0; font-weight: 600; } p { - color: #667a77; + color: var(--text-secondary); font-size: 1.1rem; margin: 0; } @@ -979,14 +638,14 @@ .dexar-category-image { width: 100%; aspect-ratio: 4 / 3; - border: 1px solid #d3dad9; - border-radius: 13px 13px 0 0; + border: 1px solid var(--border-color); + border-radius: var(--radius-lg) var(--radius-lg) 0 0; box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15); overflow: hidden; display: flex; align-items: center; justify-content: center; - background: #f5f5f5; + background: var(--bg-secondary); position: relative; img { @@ -1009,15 +668,15 @@ justify-content: center; font-size: 5rem; font-weight: 700; - color: #497671; - background: linear-gradient(135deg, #f5f5f5 0%, #e0e0e0 100%); + color: var(--primary-color); + background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%); } .dexar-category-info { width: 100%; - border: 1px solid #d3dad9; + border: 1px solid var(--border-color); border-top: none; - border-radius: 0 0 13px 13px; + border-radius: 0 0 var(--radius-lg) var(--radius-lg); padding: 12px 16px; box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.15); background: #f5f3f9; @@ -1029,10 +688,9 @@ } .dexar-category-name { - font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-weight: 600; font-size: clamp(14px, 1.4vw, 18px); - color: #1e3c38; + color: var(--text-primary); margin: 0; line-height: 1.3; display: -webkit-box; @@ -1045,10 +703,9 @@ } .dexar-category-count { - font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-weight: 600; font-size: clamp(11px, 1vw, 13px); - color: #697777; + color: var(--text-secondary); margin: 0; line-height: 1.2; } @@ -1113,16 +770,18 @@ font-size: 20px; } - .dexar-btn-primary { - width: 240px; + .dexar-btn-primary, + .dexar-btn-secondary { height: 44px; font-size: 18px; } + + .dexar-btn-primary { + width: 240px; + } .dexar-btn-secondary { width: 200px; - height: 44px; - font-size: 18px; } .dexar-categories { diff --git a/src/app/pages/home/home.component.ts b/src/app/pages/home/home.component.ts index d16165b..fb563ef 100644 --- a/src/app/pages/home/home.component.ts +++ b/src/app/pages/home/home.component.ts @@ -1,5 +1,4 @@ import { Component, OnInit, signal, computed, ChangeDetectionStrategy } from '@angular/core'; -import { CommonModule } from '@angular/common'; import { Router, RouterLink } from '@angular/router'; import { ApiService } from '../../services'; import { Category } from '../../models'; @@ -8,8 +7,7 @@ import { ItemsCarouselComponent } from '../../components/items-carousel/items-ca @Component({ selector: 'app-home', - standalone: true, - imports: [CommonModule, RouterLink, ItemsCarouselComponent], + imports: [RouterLink, ItemsCarouselComponent], templateUrl: './home.component.html', styleUrls: ['./home.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush @@ -27,6 +25,13 @@ export class HomeComponent implements OnInit { return this.categories().filter(cat => cat.parentID === 0); }); + // Memoized item count lookup + private itemCountMap = computed(() => { + const map = new Map(); + this.categories().forEach(cat => map.set(cat.categoryID, cat.itemCount || 0)); + return map; + }); + // Cache subcategories by parent ID private subcategoriesCache = computed(() => { const cache = new Map(); @@ -64,12 +69,7 @@ export class HomeComponent implements OnInit { } getItemCount(categoryID: number): number { - const cat = this.categories().find(c => c.categoryID === categoryID); - return cat?.itemCount || 0; - } - - getTopLevelCategories(): Category[] { - return this.topLevelCategories(); + return this.itemCountMap().get(categoryID) || 0; } getSubCategories(parentID: number): Category[] { diff --git a/src/app/pages/info/about/about.component.ts b/src/app/pages/info/about/about.component.ts index ec3e778..b0af9aa 100644 --- a/src/app/pages/info/about/about.component.ts +++ b/src/app/pages/info/about/about.component.ts @@ -1,11 +1,9 @@ -import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; import { environment } from '../../../../environments/environment'; @Component({ selector: 'app-about', - standalone: true, - imports: [CommonModule], + imports: [], templateUrl: './about.component.html', styleUrls: ['./about.component.scss'] }) diff --git a/src/app/pages/info/contacts/contacts.component.ts b/src/app/pages/info/contacts/contacts.component.ts index 0029236..54013e7 100644 --- a/src/app/pages/info/contacts/contacts.component.ts +++ b/src/app/pages/info/contacts/contacts.component.ts @@ -1,11 +1,9 @@ -import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; import { environment } from '../../../../environments/environment'; @Component({ selector: 'app-contacts', - standalone: true, - imports: [CommonModule], + imports: [], templateUrl: './contacts.component.html', styleUrls: ['./contacts.component.scss'] }) diff --git a/src/app/pages/info/delivery/delivery.component.ts b/src/app/pages/info/delivery/delivery.component.ts index 2397bb3..d2e3bf3 100644 --- a/src/app/pages/info/delivery/delivery.component.ts +++ b/src/app/pages/info/delivery/delivery.component.ts @@ -1,11 +1,9 @@ -import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; import { environment } from '../../../../environments/environment'; @Component({ selector: 'app-delivery', - standalone: true, - imports: [CommonModule], + imports: [], templateUrl: './delivery.component.html', styleUrls: ['./delivery.component.scss'] }) diff --git a/src/app/pages/info/faq/faq.component.ts b/src/app/pages/info/faq/faq.component.ts index 5d19238..7a05eb5 100644 --- a/src/app/pages/info/faq/faq.component.ts +++ b/src/app/pages/info/faq/faq.component.ts @@ -1,11 +1,9 @@ -import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; import { environment } from '../../../../environments/environment'; @Component({ selector: 'app-faq', - standalone: true, - imports: [CommonModule], + imports: [], templateUrl: './faq.component.html', styleUrls: ['./faq.component.scss'] }) diff --git a/src/app/pages/info/guarantee/guarantee.component.ts b/src/app/pages/info/guarantee/guarantee.component.ts index 49ef85e..3ae2e97 100644 --- a/src/app/pages/info/guarantee/guarantee.component.ts +++ b/src/app/pages/info/guarantee/guarantee.component.ts @@ -1,11 +1,9 @@ -import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; import { environment } from '../../../../environments/environment'; @Component({ selector: 'app-guarantee', - standalone: true, - imports: [CommonModule], + imports: [], templateUrl: './guarantee.component.html', styleUrls: ['./guarantee.component.scss'] }) diff --git a/src/app/pages/item-detail/item-detail.component.ts b/src/app/pages/item-detail/item-detail.component.ts index 0b63cec..3ed8b25 100644 --- a/src/app/pages/item-detail/item-detail.component.ts +++ b/src/app/pages/item-detail/item-detail.component.ts @@ -1,5 +1,5 @@ -import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy } 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'; @@ -10,8 +10,7 @@ import { environment } from '../../../environments/environment'; @Component({ selector: 'app-item-detail', - standalone: true, - imports: [CommonModule, RouterLink, FormsModule], + imports: [DecimalPipe, RouterLink, FormsModule], templateUrl: './item-detail.component.html', styleUrls: ['./item-detail.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush @@ -92,8 +91,8 @@ export class ItemDetailComponent implements OnInit, OnDestroy { 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; } @@ -103,10 +102,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', @@ -121,7 +120,7 @@ export class ItemDetailComponent implements OnInit, OnDestroy { getUserDisplayName(): string | null { if (!this.telegramService.isTelegramApp()) { - return 'Пользователь'; + return '������������'; } return this.telegramService.getDisplayName(); } @@ -150,12 +149,12 @@ export class ItemDetailComponent implements OnInit, OnDestroy { this.reviewSubmitStatus.set('success'); this.newReview = { rating: 0, comment: '', anonymous: false }; - // Скрыть сообщение через 3 секунды + // ������ ��������� ����� 3 ������� setTimeout(() => { this.reviewSubmitStatus.set('idle'); }, 3000); - // Перезагрузить данные товара через небольшую задержку + // ������������� ������ ������ ����� ��������� �������� setTimeout(() => { this.loadItem(currentItem.itemID); }, 500); @@ -164,7 +163,7 @@ export class ItemDetailComponent implements OnInit, OnDestroy { console.error('Error submitting review:', err); this.reviewSubmitStatus.set('error'); - // Скрыть сообщение об ошибке через 5 секунд + // ������ ��������� �� ������ ����� 5 ������ 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 9d49859..ce23538 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,9 @@ -import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; import { environment } from '../../../../environments/environment'; @Component({ selector: 'app-company-details', - standalone: true, - imports: [CommonModule], + imports: [], templateUrl: './company-details.component.html', styleUrls: ['./company-details.component.scss'] }) 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 fd12759..aa841e5 100644 --- a/src/app/pages/legal/payment-terms/payment-terms.component.ts +++ b/src/app/pages/legal/payment-terms/payment-terms.component.ts @@ -1,12 +1,10 @@ -import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; import { RouterLink } from '@angular/router'; import { environment } from '../../../../environments/environment'; @Component({ selector: 'app-payment-terms', - standalone: true, - imports: [CommonModule, RouterLink], + imports: [RouterLink], templateUrl: './payment-terms.component.html', styleUrls: ['./payment-terms.component.scss'] }) 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 3b421e0..f0841ac 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,9 @@ -import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; import { environment } from '../../../../environments/environment'; @Component({ selector: 'app-privacy-policy', - standalone: true, - imports: [CommonModule], + imports: [], templateUrl: './privacy-policy.component.html', styleUrls: ['./privacy-policy.component.scss'] }) 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 2fb838c..71cb01c 100644 --- a/src/app/pages/legal/public-offer/public-offer.component.ts +++ b/src/app/pages/legal/public-offer/public-offer.component.ts @@ -1,12 +1,10 @@ -import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; import { RouterLink } from '@angular/router'; import { environment } from '../../../../environments/environment'; @Component({ selector: 'app-public-offer', - standalone: true, - imports: [CommonModule, RouterLink], + imports: [RouterLink], templateUrl: './public-offer.component.html', styleUrls: ['./public-offer.component.scss'] }) 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 2be30c5..a8faa7e 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,10 @@ -import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; import { RouterLink } from '@angular/router'; import { environment } from '../../../../environments/environment'; @Component({ selector: 'app-return-policy', - standalone: true, - imports: [CommonModule], + imports: [], templateUrl: './return-policy.component.html', styleUrls: ['./return-policy.component.scss'] }) diff --git a/src/app/pages/search/search.component.ts b/src/app/pages/search/search.component.ts index e32178d..570b61d 100644 --- a/src/app/pages/search/search.component.ts +++ b/src/app/pages/search/search.component.ts @@ -1,16 +1,16 @@ import { Component, signal, HostListener, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { DecimalPipe } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { RouterLink } from '@angular/router'; import { ApiService, CartService } from '../../services'; import { Item } from '../../models'; import { Subject, Subscription } from 'rxjs'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; +import { getDiscountedPrice, getMainImage, trackByItemId } from '../../utils/item.utils'; @Component({ selector: 'app-search', - standalone: true, - imports: [CommonModule, FormsModule, RouterLink], + imports: [DecimalPipe, FormsModule, RouterLink], templateUrl: './search.component.html', styleUrls: ['./search.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush @@ -39,7 +39,9 @@ export class SearchComponent implements OnDestroy { distinctUntilChanged() ) .subscribe(query => { - this.performSearch(query); + if (query.trim().length >= 3 || query.trim().length === 0) { + this.performSearch(query); + } }); } @@ -126,16 +128,7 @@ export class SearchComponent implements OnDestroy { this.cartService.addItem(itemID); } - getDiscountedPrice(item: Item): number { - return item.price * (1 - item.discount / 100); - } - - getMainImage(item: Item): string { - return item.photos?.[0]?.url || ''; - } - - // TrackBy function for performance optimization - trackByItemId(index: number, item: Item): number { - return item.itemID; - } + readonly getDiscountedPrice = getDiscountedPrice; + readonly getMainImage = getMainImage; + readonly trackByItemId = trackByItemId; } diff --git a/src/app/services/cart.service.ts b/src/app/services/cart.service.ts index 4915441..f430b64 100644 --- a/src/app/services/cart.service.ts +++ b/src/app/services/cart.service.ts @@ -1,13 +1,14 @@ import { Injectable, signal, computed, effect } from '@angular/core'; import { ApiService } from './api.service'; import { Item, CartItem } from '../models'; +import { environment } from '../../environments/environment'; import type { } from '../types/telegram.types'; @Injectable({ providedIn: 'root' }) export class CartService { - private readonly STORAGE_KEY = 'dexarmarket_cart'; + private readonly STORAGE_KEY = `${environment.brandName.toLowerCase().replace(/\s+/g, '_')}_cart`; private cartItems = signal([]); private isTelegram = typeof window !== 'undefined' && !!window.Telegram?.WebApp; @@ -60,29 +61,13 @@ export class CartService { console.error('Error loading from Telegram CloudStorage:', err); this.loadFromSessionStorage(); } else if (value) { - try { - const items = JSON.parse(value); - if (Array.isArray(items)) { - // Migrate old items without quantity field - const migratedItems: CartItem[] = items.map(item => ({ - ...item, - quantity: item.quantity || 1 - })); - this.cartItems.set(migratedItems); - } else { - this.cartItems.set([]); - } - } catch (parseErr) { - console.error('Error parsing cart data:', parseErr); - this.loadFromSessionStorage(); - } + this.parseAndSetCart(value) || this.loadFromSessionStorage(); } else { // No data in CloudStorage, try sessionStorage this.loadFromSessionStorage(); } }); } else { - // Load from sessionStorage this.loadFromSessionStorage(); } } @@ -90,27 +75,28 @@ export class CartService { private loadFromSessionStorage(): void { const stored = sessionStorage.getItem(this.STORAGE_KEY); if (stored) { - try { - const items = JSON.parse(stored); - if (Array.isArray(items)) { - // Migrate old items without quantity field - const migratedItems: CartItem[] = items.map(item => ({ - ...item, - quantity: item.quantity || 1 - })); - this.cartItems.set(migratedItems); - } else { - this.cartItems.set([]); - } - } catch (err) { - console.error('Error parsing cart from sessionStorage:', err); - this.cartItems.set([]); - } - } else { - this.cartItems.set([]); + this.parseAndSetCart(stored); } } + /** Parse JSON cart data, migrate legacy items, and set the signal. Returns true on success. */ + private parseAndSetCart(json: string): boolean { + try { + const items = JSON.parse(json); + if (Array.isArray(items)) { + this.cartItems.set(items.map(item => ({ + ...item, + quantity: item.quantity || 1 + }))); + return true; + } + } catch (err) { + console.error('Error parsing cart data:', err); + } + this.cartItems.set([]); + return false; + } + addItem(itemID: number, quantity: number = 1): void { const currentItems = this.cartItems(); const existingItem = currentItems.find(i => i.itemID === itemID); diff --git a/src/app/utils/item.utils.ts b/src/app/utils/item.utils.ts new file mode 100644 index 0000000..dcd7809 --- /dev/null +++ b/src/app/utils/item.utils.ts @@ -0,0 +1,13 @@ +import { Item } from '../models'; + +export function getDiscountedPrice(item: Item): number { + return item.price * (1 - item.discount / 100); +} + +export function getMainImage(item: Item): string { + return item.photos?.[0]?.url || ''; +} + +export function trackByItemId(index: number, item: Item): number { + return item.itemID; +}