improvments are done
This commit is contained in:
@@ -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';
|
||||
@@ -12,7 +12,6 @@ export const appConfig: ApplicationConfig = {
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideRouter(
|
||||
routes,
|
||||
withPreloading(PreloadAllModules),
|
||||
withInMemoryScrolling({ scrollPositionRestoration: 'top' })
|
||||
),
|
||||
provideHttpClient(
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
@if (checkingServer()) {
|
||||
<div class="server-check-overlay">
|
||||
<div class="server-check-content">
|
||||
<div class="spinner-large"></div>
|
||||
<h2>Проверка соединения с сервером...</h2>
|
||||
</div>
|
||||
<div class="spinner-large"></div>
|
||||
<p>Подключение к серверу...</p>
|
||||
</div>
|
||||
} @else if (!serverAvailable()) {
|
||||
<div class="server-error-overlay">
|
||||
<div class="server-error-content">
|
||||
<div class="error-icon">⚠️</div>
|
||||
<h1>Извините, возникла проблема</h1>
|
||||
<p>Не удается подключиться к серверу. Пожалуйста, проверьте подключение к интернету или попробуйте позже.</p>
|
||||
<button class="retry-btn" (click)="retryConnection()">Попробовать снова</button>
|
||||
</div>
|
||||
<div class="error-icon">⚠️</div>
|
||||
<h2>Сервер недоступен</h2>
|
||||
<p>Не удалось подключиться к серверу. Проверьте подключение к интернету.</p>
|
||||
<button class="retry-btn" (click)="retryConnection()">Повторить попытку</button>
|
||||
</div>
|
||||
} @else {
|
||||
<app-header></app-header>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
114
src/app/app.ts
114
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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -4,7 +4,6 @@ import { environment } from '../../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-back-button',
|
||||
standalone: true,
|
||||
template: `
|
||||
@if (!isnovo) {
|
||||
<button class="dexar-back-btn" (click)="goBack()" aria-label="Назад">
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-empty-cart-icon',
|
||||
standalone: true,
|
||||
templateUrl: './empty-cart-icon.component.html',
|
||||
styleUrls: ['./empty-cart-icon.component.scss']
|
||||
})
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-footer',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterLink],
|
||||
imports: [RouterLink],
|
||||
templateUrl: './footer.component.html',
|
||||
styleUrls: ['./footer.component.scss']
|
||||
styleUrls: ['./footer.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class FooterComponent {
|
||||
currentYear = new Date().getFullYear();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Router, RouterLink, RouterLinkActive } from '@angular/router';
|
||||
import { CartService } from '../../services';
|
||||
import { environment } from '../../../environments/environment';
|
||||
@@ -8,10 +7,10 @@ import { LanguageSelectorComponent } from '../language-selector/language-selecto
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterLink, RouterLinkActive, LogoComponent, LanguageSelectorComponent],
|
||||
imports: [RouterLink, RouterLinkActive, LogoComponent, LanguageSelectorComponent],
|
||||
templateUrl: './header.component.html',
|
||||
styleUrls: ['./header.component.scss']
|
||||
styleUrls: ['./header.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class HeaderComponent {
|
||||
cartItemCount;
|
||||
|
||||
@@ -58,7 +58,6 @@
|
||||
</a>
|
||||
</div>
|
||||
</ng-template>
|
||||
<ul class="p-carousel-indicator-list" data-pc-section="indicatorlist"><li class="p-carousel-indicator" data-p-active="false" data-pc-section="indicator"><button type="button" class="p-carousel-indicator-button" tabindex="-1" aria-label="1" data-pc-section="indicatorbutton"></button></li><li class="p-carousel-indicator" data-p-active="false" data-pc-section="indicator"><button type="button" class="p-carousel-indicator-button" tabindex="-1" aria-label="2" data-pc-section="indicatorbutton"></button></li><li class="p-carousel-indicator" data-p-active="false" data-pc-section="indicator"><button type="button" class="p-carousel-indicator-button" tabindex="-1" aria-label="3" data-pc-section="indicatorbutton"></button></li><li class="p-carousel-indicator" data-p-active="false" data-pc-section="indicator"><button type="button" class="p-carousel-indicator-button" tabindex="-1" aria-label="4" data-pc-section="indicatorbutton"></button></li><li class="p-carousel-indicator" data-p-active="false" data-pc-section="indicator"><button type="button" class="p-carousel-indicator-button" tabindex="-1" aria-label="5" data-pc-section="indicatorbutton"></button></li><li class="p-carousel-indicator" data-p-active="false" data-pc-section="indicator"><button type="button" class="p-carousel-indicator-button" tabindex="-1" aria-label="6" data-pc-section="indicatorbutton"></button></li><li class="p-carousel-indicator p-carousel-indicator-active" data-p-active="true" data-pc-section="indicator"><button type="button" class="p-carousel-indicator-button" tabindex="0" aria-label="7" aria-current="page" data-pc-section="indicatorbutton"></button></li><!----></ul>
|
||||
</p-carousel>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -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<Item[]>([]);
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -3,7 +3,6 @@ import { environment } from '../../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-logo',
|
||||
standalone: true,
|
||||
template: `<img [src]="logoPath" [alt]="brandName + ' logo'" class="logo-img" fetchpriority="high" />`,
|
||||
styles: [`
|
||||
.logo-img {
|
||||
|
||||
@@ -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('<EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>?')) {
|
||||
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('<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>.');
|
||||
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('<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -325,7 +316,7 @@ export class CartComponent implements OnInit, OnDestroy {
|
||||
next: () => {
|
||||
this.emailSubmitting.set(false);
|
||||
// Show success message
|
||||
alert('Email успешно отправлен! Проверьте вашу почту.');
|
||||
alert('Email <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>! <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>.');
|
||||
// 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('<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> email. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>.');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -396,11 +387,11 @@ export class CartComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
if (digitsOnly.length === 0) {
|
||||
this.phoneError.set('Номер телефона обязателен');
|
||||
this.phoneError.set('<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>');
|
||||
} else if (digitsOnly.length < 11) {
|
||||
this.phoneError.set(`Введите еще ${11 - digitsOnly.length} цифр`);
|
||||
this.phoneError.set(`<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> ${11 - digitsOnly.length} <EFBFBD><EFBFBD><EFBFBD><EFBFBD>`);
|
||||
} else if (digitsOnly.length > 11) {
|
||||
this.phoneError.set('Слишком много цифр');
|
||||
this.phoneError.set('<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>');
|
||||
} 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 <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>');
|
||||
} else if (email.length < 5) {
|
||||
this.emailError.set('Email слишком короткий (минимум 5 символов)');
|
||||
this.emailError.set('Email <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 5 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>)');
|
||||
} else if (email.length > 100) {
|
||||
this.emailError.set('Email слишком длинный (максимум 100 символов)');
|
||||
this.emailError.set('Email <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> 100 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>)');
|
||||
} else if (!email.includes('@')) {
|
||||
this.emailError.set('Email должен содержать @');
|
||||
this.emailError.set('Email <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> @');
|
||||
} else if (!email.includes('.')) {
|
||||
this.emailError.set('Email должен содержать домен (.com, .ru и т.д.)');
|
||||
this.emailError.set('Email <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> (.com, .ru <EFBFBD> <20>.<2E>.)');
|
||||
} else {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
this.emailError.set('Некорректный формат email');
|
||||
this.emailError.set('<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> email');
|
||||
} else {
|
||||
this.emailError.set('');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<p>Выберите интересующую категорию</p>
|
||||
</div>
|
||||
|
||||
@if (getTopLevelCategories().length === 0) {
|
||||
@if (topLevelCategories().length === 0) {
|
||||
<div class="novo-empty">
|
||||
<div class="novo-empty-icon">📦</div>
|
||||
<h3>Категории скоро появятся</h3>
|
||||
@@ -49,7 +49,7 @@
|
||||
</div>
|
||||
} @else {
|
||||
<div class="novo-categories-grid">
|
||||
@for (category of getTopLevelCategories(); track category.categoryID) {
|
||||
@for (category of topLevelCategories(); track category.categoryID) {
|
||||
<a [routerLink]="['/category', category.categoryID]" class="novo-category-card">
|
||||
<div class="novo-category-image">
|
||||
@if (category.icon) {
|
||||
@@ -117,7 +117,7 @@
|
||||
@if (!loading() && !error()) {
|
||||
<section class="dexar-categories" id="catalog">
|
||||
<h2 class="dexar-categories-title">Каталог товаров</h2>
|
||||
@if (getTopLevelCategories().length === 0) {
|
||||
@if (topLevelCategories().length === 0) {
|
||||
<div class="dexar-empty-categories">
|
||||
<div class="dexar-empty-icon">📦</div>
|
||||
<h3>Категории пока отсутствуют</h3>
|
||||
@@ -125,7 +125,7 @@
|
||||
</div>
|
||||
} @else {
|
||||
<div class="dexar-categories-grid">
|
||||
@for (category of getTopLevelCategories(); track category.categoryID) {
|
||||
@for (category of topLevelCategories(); track category.categoryID) {
|
||||
<a [routerLink]="['/category', category.categoryID]"
|
||||
class="dexar-category-card"
|
||||
[class.dexar-category-card--wide]="isWideCategory(category.categoryID)">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<number, number>();
|
||||
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<number, Category[]>();
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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 '<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>';
|
||||
if (diffDays === 1) return '<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>';
|
||||
if (diffDays < 7) return `${diffDays} <EFBFBD><EFBFBD>. <20><><EFBFBD><EFBFBD><EFBFBD>`;
|
||||
if (diffDays < 30) return `${Math.floor(diffDays / 7)} <EFBFBD><EFBFBD><EFBFBD>. <20><><EFBFBD><EFBFBD><EFBFBD>`;
|
||||
|
||||
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 '<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>';
|
||||
}
|
||||
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 секунды
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> 3 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
setTimeout(() => {
|
||||
this.reviewSubmitStatus.set('idle');
|
||||
}, 3000);
|
||||
|
||||
// Перезагрузить данные товара через небольшую задержку
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
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 секунд
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> 5 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
setTimeout(() => {
|
||||
this.reviewSubmitStatus.set('idle');
|
||||
}, 5000);
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<CartItem[]>([]);
|
||||
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);
|
||||
|
||||
13
src/app/utils/item.utils.ts
Normal file
13
src/app/utils/item.utils.ts
Normal file
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user