improvments are done

This commit is contained in:
sdarbinyan
2026-02-19 01:23:25 +04:00
parent e3efb270dd
commit 18df968b7a
42 changed files with 281 additions and 744 deletions

View File

@@ -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('');
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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)">

View File

@@ -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 {

View File

@@ -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[] {

View File

@@ -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']
})

View File

@@ -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']
})

View File

@@ -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']
})

View File

@@ -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']
})

View File

@@ -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']
})

View File

@@ -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);

View File

@@ -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']
})

View File

@@ -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']
})

View File

@@ -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']
})

View File

@@ -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']
})

View File

@@ -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']
})

View File

@@ -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;
}