added translations
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
<div [class]="isnovo ? 'cart-container novo' : 'cart-container dexar'">
|
||||
<div class="cart-header">
|
||||
<h1>Корзина</h1>
|
||||
<h1>{{ 'cart.title' | translate }}</h1>
|
||||
@if (itemCount() > 0) {
|
||||
<button class="clear-cart-btn" (click)="clearCart()">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/>
|
||||
</svg>
|
||||
Очистить
|
||||
{{ 'cart.clear' | translate }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
@@ -16,9 +16,9 @@
|
||||
<div class="empty-icon">
|
||||
<app-empty-cart-icon />
|
||||
</div>
|
||||
<h2>Корзина пуста</h2>
|
||||
<p>Добавьте товары, чтобы начать покупки</p>
|
||||
<a [routerLink]="'/' | langRoute" class="shop-btn">Перейти к покупкам</a>
|
||||
<h2>{{ 'cart.empty' | translate }}</h2>
|
||||
<p>{{ 'cart.emptyDesc' | translate }}</p>
|
||||
<a [routerLink]="'/' | langRoute" class="shop-btn">{{ 'cart.goShopping' | translate }}</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -86,21 +86,21 @@
|
||||
|
||||
<div class="cart-summary">
|
||||
<div class="summary-header">
|
||||
<h3>Итого</h3>
|
||||
<h3>{{ 'cart.total' | translate }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="summary-row">
|
||||
<span>Товары ({{ itemCount() }})</span>
|
||||
<span>{{ 'cart.items' | translate }} ({{ itemCount() }})</span>
|
||||
<span class="value">{{ totalPrice() | number:'1.2-2' }} ₽</span>
|
||||
</div>
|
||||
|
||||
<div class="summary-row delivery">
|
||||
<span>Доставка</span>
|
||||
<span>{{ 'cart.deliveryLabel' | translate }}</span>
|
||||
<span>0 ₽</span>
|
||||
</div>
|
||||
|
||||
<div class="summary-row total">
|
||||
<span>К оплате</span>
|
||||
<span>{{ 'cart.toPay' | translate }}</span>
|
||||
<span class="total-price">{{ totalPrice() | number:'1.2-2' }} ₽</span>
|
||||
</div>
|
||||
|
||||
@@ -113,11 +113,11 @@
|
||||
/>
|
||||
<span class="checkmark"></span>
|
||||
<span class="terms-text">
|
||||
Я согласен с
|
||||
<a [routerLink]="'/public-offer' | langRoute" target="_blank">публичной офертой</a>,
|
||||
<a [routerLink]="'/return-policy' | langRoute" target="_blank">политикой возврата</a>,
|
||||
<a [routerLink]="'/guarantee' | langRoute" target="_blank">условиями гарантии</a> и
|
||||
<a [routerLink]="'/privacy-policy' | langRoute" target="_blank">политикой конфиденциальности</a>
|
||||
{{ 'cart.agreeWith' | translate }}
|
||||
<a [routerLink]="'/public-offer' | langRoute" target="_blank">{{ 'cart.publicOffer' | translate }}</a>,
|
||||
<a [routerLink]="'/return-policy' | langRoute" target="_blank">{{ 'cart.returnPolicy' | translate }}</a>,
|
||||
<a [routerLink]="'/guarantee' | langRoute" target="_blank">{{ 'cart.guaranteeTerms' | translate }}</a> {{ 'cart.and' | translate }}
|
||||
<a [routerLink]="'/privacy-policy' | langRoute" target="_blank">{{ 'cart.privacyPolicy' | translate }}</a>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -128,7 +128,7 @@
|
||||
[class.disabled]="!termsAccepted"
|
||||
[disabled]="!termsAccepted"
|
||||
>
|
||||
Оформить заказ
|
||||
{{ 'cart.checkout' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,7 +139,7 @@
|
||||
@if (showPaymentPopup()) {
|
||||
<div class="payment-modal-overlay">
|
||||
<div class="payment-modal">
|
||||
<button class="close-modal-btn" (click)="closePaymentPopup()" aria-label="Закрыть">
|
||||
<button class="close-modal-btn" (click)="closePaymentPopup()" [attr.aria-label]="'cart.close' | translate">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M6 6L18 18M6 18L18 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -147,14 +147,14 @@
|
||||
@if (paymentStatus() === 'creating') {
|
||||
<div class="payment-status-screen">
|
||||
<div class="spinner-large"></div>
|
||||
<h2>Создание платежа...</h2>
|
||||
<p>Подождите несколько секунд</p>
|
||||
<h2>{{ 'cart.creatingPayment' | translate }}</h2>
|
||||
<p>{{ 'cart.waitFewSeconds' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (paymentStatus() === 'waiting') {
|
||||
<div class="payment-active">
|
||||
<h2>Сканируйте QR-код для оплаты</h2>
|
||||
<h2>{{ 'cart.scanQr' | translate }}</h2>
|
||||
|
||||
<div class="qr-section">
|
||||
<div class="qr-wrapper">
|
||||
@@ -165,22 +165,22 @@
|
||||
|
||||
<div class="payment-info">
|
||||
<div class="payment-amount">
|
||||
<span class="label">Сумма к оплате:</span>
|
||||
<span class="label">{{ 'cart.amountToPay' | translate }}</span>
|
||||
<span class="amount">{{ totalPrice() | number:'1.2-2' }} RUB</span>
|
||||
</div>
|
||||
|
||||
<div class="waiting-indicator">
|
||||
<div class="pulse-dot"></div>
|
||||
<span>Ожидание оплаты...</span>
|
||||
<span>{{ 'cart.waitingPayment' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="payment-actions">
|
||||
<button class="copy-btn" (click)="copyPaymentLink()">
|
||||
{{ linkCopied() ? '✓ Скопировано' : 'Скопировать ссылку' }}
|
||||
{{ linkCopied() ? ('cart.copied' | translate) : ('cart.copyLink' | translate) }}
|
||||
</button>
|
||||
<a [href]="paymentUrl()" target="_blank" rel="noopener noreferrer" class="open-btn">
|
||||
Открыть в новой вкладке
|
||||
{{ 'cart.openNewTab' | translate }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,8 +189,8 @@
|
||||
@if (paymentStatus() === 'success') {
|
||||
<div class="payment-status-screen success">
|
||||
<div class="success-icon">✓</div>
|
||||
<h2>Поздравляем! Оплата прошла успешно!</h2>
|
||||
<p class="success-text">Введите ваши контактные данные, и мы отправим вам покупку в течение нескольких минут</p>
|
||||
<h2>{{ 'cart.paymentSuccess' | translate }}</h2>
|
||||
<p class="success-text">{{ 'cart.paymentSuccessDesc' | translate }}</p>
|
||||
|
||||
<div class="email-form">
|
||||
<div class="input-group">
|
||||
@@ -236,9 +236,9 @@
|
||||
>
|
||||
@if (emailSubmitting()) {
|
||||
<span class="spinner-small"></span>
|
||||
Отправка...
|
||||
{{ 'cart.sending' | translate }}
|
||||
} @else {
|
||||
Отправить
|
||||
{{ 'cart.send' | translate }}
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
@@ -248,9 +248,9 @@
|
||||
@if (paymentStatus() === 'timeout') {
|
||||
<div class="payment-status-screen timeout">
|
||||
<div class="timeout-icon">⏱</div>
|
||||
<h2>Время ожидания истекло</h2>
|
||||
<p>Мы не получили подтверждение оплаты в течение 3 минут.</p>
|
||||
<p class="auto-close">Окно закроется автоматически...</p>
|
||||
<h2>{{ 'cart.paymentTimeout' | translate }}</h2>
|
||||
<p>{{ 'cart.paymentTimeoutDesc' | translate }}</p>
|
||||
<p class="auto-close">{{ 'cart.autoClose' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, computed, ChangeDetectionStrategy, signal, OnDestroy } from '@angular/core';
|
||||
import { Component, computed, ChangeDetectionStrategy, signal, OnDestroy, inject } from '@angular/core';
|
||||
import { DecimalPipe } from '@angular/common';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
@@ -10,10 +10,12 @@ import { EmptyCartIconComponent } from '../../components/empty-cart-icon/empty-c
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { getDiscountedPrice, getMainImage, trackByItemId } from '../../utils/item.utils';
|
||||
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
|
||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||
import { TranslateService } from '../../i18n/translate.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-cart',
|
||||
imports: [DecimalPipe, RouterLink, FormsModule, EmptyCartIconComponent, LangRoutePipe],
|
||||
imports: [DecimalPipe, RouterLink, FormsModule, EmptyCartIconComponent, LangRoutePipe, TranslatePipe],
|
||||
templateUrl: './cart.component.html',
|
||||
styleUrls: ['./cart.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
@@ -25,6 +27,8 @@ export class CartComponent implements OnDestroy {
|
||||
termsAccepted = false;
|
||||
isnovo = environment.theme === 'novo';
|
||||
|
||||
private i18n = inject(TranslateService);
|
||||
|
||||
// Swipe state
|
||||
swipedItemId = signal<number | null>(null);
|
||||
|
||||
@@ -116,7 +120,7 @@ export class CartComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
clearCart(): void {
|
||||
if (confirm('Вы уверены, что хотите очистить корзину?')) {
|
||||
if (confirm(this.i18n.t('cart.confirmClear'))) {
|
||||
this.cartService.clearCart();
|
||||
}
|
||||
}
|
||||
@@ -127,7 +131,7 @@ export class CartComponent implements OnDestroy {
|
||||
|
||||
checkout(): void {
|
||||
if (!this.termsAccepted) {
|
||||
alert('Пожалуйста, примите условия оферты, политику возврата и возврата для подтверждения оформления заказа.');
|
||||
alert(this.i18n.t('cart.acceptTerms'));
|
||||
return;
|
||||
}
|
||||
this.openPaymentPopup();
|
||||
@@ -249,7 +253,7 @@ export class CartComponent implements OnDestroy {
|
||||
this.linkCopied.set(true);
|
||||
setTimeout(() => this.linkCopied.set(false), 2000);
|
||||
}).catch(err => {
|
||||
console.error('Ошибка копирования:', err);
|
||||
console.error(this.i18n.t('cart.copyError'), err);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -314,7 +318,7 @@ export class CartComponent implements OnDestroy {
|
||||
next: () => {
|
||||
this.emailSubmitting.set(false);
|
||||
// Show success message
|
||||
alert('Email успешно отправлен! Проверьте свою почту.');
|
||||
alert(this.i18n.t('cart.emailSuccess'));
|
||||
// Close popup and redirect to home page
|
||||
setTimeout(() => {
|
||||
this.closePaymentPopup();
|
||||
@@ -325,7 +329,7 @@ export class CartComponent implements OnDestroy {
|
||||
error: (err) => {
|
||||
console.error('Error submitting email:', err);
|
||||
this.emailSubmitting.set(false);
|
||||
alert('Произошла ошибка при отправке email. Пожалуйста, попробуйте снова.');
|
||||
alert(this.i18n.t('cart.emailError'));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -386,11 +390,11 @@ export class CartComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
if (digitsOnly.length === 0) {
|
||||
this.phoneError.set('Номер телефона обязателен');
|
||||
this.phoneError.set(this.i18n.t('cart.phoneRequired'));
|
||||
} else if (digitsOnly.length < 11) {
|
||||
this.phoneError.set(`Введите ещё ${11 - digitsOnly.length} цифр`);
|
||||
this.phoneError.set(this.i18n.t('cart.phoneMoreDigits', { count: 11 - digitsOnly.length }));
|
||||
} else if (digitsOnly.length > 11) {
|
||||
this.phoneError.set('Слишком много цифр');
|
||||
this.phoneError.set(this.i18n.t('cart.phoneTooMany'));
|
||||
} else {
|
||||
this.phoneError.set('');
|
||||
}
|
||||
@@ -418,19 +422,19 @@ export class CartComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
if (email.length === 0) {
|
||||
this.emailError.set('Email обязателен');
|
||||
this.emailError.set(this.i18n.t('cart.emailRequired'));
|
||||
} else if (email.length < 5) {
|
||||
this.emailError.set('Email слишком короткий (минимум 5 символов)');
|
||||
this.emailError.set(this.i18n.t('cart.emailTooShort'));
|
||||
} else if (email.length > 100) {
|
||||
this.emailError.set('Email слишком длинный (максимум 100 символов)');
|
||||
this.emailError.set(this.i18n.t('cart.emailTooLong'));
|
||||
} else if (!email.includes('@')) {
|
||||
this.emailError.set('Email должен содержать @');
|
||||
this.emailError.set(this.i18n.t('cart.emailNeedsAt'));
|
||||
} else if (!email.includes('.')) {
|
||||
this.emailError.set('Email должен содержать домен (.com, .ru и т.д.)');
|
||||
this.emailError.set(this.i18n.t('cart.emailNeedsDomain'));
|
||||
} else {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
this.emailError.set('Некорректный формат email');
|
||||
this.emailError.set(this.i18n.t('cart.emailInvalid'));
|
||||
} else {
|
||||
this.emailError.set('');
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@if (error()) {
|
||||
<div class="error">
|
||||
<p>{{ error() }}</p>
|
||||
<button (click)="resetAndLoad()">Попробовать снова</button>
|
||||
<button (click)="resetAndLoad()">{{ 'category.retry' | translate }}</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</a>
|
||||
|
||||
<button class="add-to-cart-btn" (click)="addToCart(item.itemID, $event)">
|
||||
В корзину
|
||||
{{ 'category.addToCart' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@@ -55,13 +55,13 @@
|
||||
@if (loading() && items().length > 0) {
|
||||
<div class="loading-more">
|
||||
<div class="spinner"></div>
|
||||
<p>Загрузка...</p>
|
||||
<p>{{ 'category.loadingMore' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!hasMore() && items().length > 0) {
|
||||
<div class="no-more">
|
||||
<p>Все товары загружены</p>
|
||||
<p>{{ 'category.allLoaded' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -75,16 +75,16 @@
|
||||
<path d="M10 14H14" stroke="#d3dad9" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Упс! Здесь пока пусто</h3>
|
||||
<p>В этой категории ещё нет товаров, но скоро они появятся</p>
|
||||
<a [routerLink]="'/' | langRoute" class="no-items-btn">На главную</a>
|
||||
<h3>{{ 'category.emptyTitle' | translate }}</h3>
|
||||
<p>{{ 'category.emptyDesc' | translate }}</p>
|
||||
<a [routerLink]="'/' | langRoute" class="no-items-btn">{{ 'category.goHome' | translate }}</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (loading() && items().length === 0) {
|
||||
<div class="loading-initial">
|
||||
<div class="spinner"></div>
|
||||
<p>Загрузка товаров...</p>
|
||||
<p>{{ 'category.loading' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@ import { Item } from '../../models';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { getDiscountedPrice, getMainImage, trackByItemId } from '../../utils/item.utils';
|
||||
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
|
||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-category',
|
||||
imports: [DecimalPipe, RouterLink, LangRoutePipe],
|
||||
imports: [DecimalPipe, RouterLink, LangRoutePipe, TranslatePipe],
|
||||
templateUrl: './category.component.html',
|
||||
styleUrls: ['./category.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
@if (loading()) {
|
||||
<div class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Загрузка подкатегорий...</p>
|
||||
<p>{{ 'subcategories.loading' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (error()) {
|
||||
<div class="error">
|
||||
<p>{{ error() }}</p>
|
||||
<button (click)="ngOnInit()">Попробовать снова</button>
|
||||
<button (click)="ngOnInit()">{{ 'subcategories.retry' | translate }}</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -45,9 +45,9 @@
|
||||
<rect x="14" y="14" width="7" height="7" rx="1.5" stroke="#d3dad9" stroke-width="1.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3>Упс! Подкатегорий пока нет</h3>
|
||||
<p>В этом разделе ещё нет подкатегорий, но скоро они появятся</p>
|
||||
<a [routerLink]="'/' | langRoute" class="no-subcats-btn">На главную</a>
|
||||
<h3>{{ 'subcategories.emptyTitle' | translate }}</h3>
|
||||
<p>{{ 'subcategories.emptyDesc' | translate }}</p>
|
||||
<a [routerLink]="'/' | langRoute" class="no-subcats-btn">{{ 'subcategories.goHome' | translate }}</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy, inject } from '@angular/core';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { ApiService, LanguageService } from '../../services';
|
||||
import { Category } from '../../models';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
|
||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||
import { TranslateService } from '../../i18n/translate.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-subcategories',
|
||||
imports: [RouterLink, LangRoutePipe],
|
||||
imports: [RouterLink, LangRoutePipe, TranslatePipe],
|
||||
templateUrl: './subcategories.component.html',
|
||||
styleUrls: ['./subcategories.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
@@ -17,6 +19,8 @@ export class SubcategoriesComponent implements OnInit, OnDestroy {
|
||||
subcategories = signal<Category[]>([]);
|
||||
loading = signal(true);
|
||||
error = signal<string | null>(null);
|
||||
|
||||
private i18n = inject(TranslateService);
|
||||
parentName = signal<string>('');
|
||||
|
||||
private routeSubscription?: Subscription;
|
||||
@@ -46,7 +50,7 @@ export class SubcategoriesComponent implements OnInit, OnDestroy {
|
||||
this.categories.set(cats);
|
||||
const subs = cats.filter(c => c.parentID === parentID);
|
||||
const parent = cats.find(c => c.categoryID === parentID);
|
||||
this.parentName.set(parent ? parent.name : 'Категория');
|
||||
this.parentName.set(parent ? parent.name : this.i18n.t('home.categoriesTitle'));
|
||||
|
||||
if (!subs || subs.length === 0) {
|
||||
// No subcategories: redirect to items list for this category
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
<div class="novo-home">
|
||||
<section class="novo-hero novo-hero-compact">
|
||||
<div class="novo-hero-content">
|
||||
<h1 class="novo-hero-title">Добро пожаловать в {{ brandName }}</h1>
|
||||
<p class="novo-hero-subtitle">Найдите всё, что нужно, в одном месте</p>
|
||||
<h1 class="novo-hero-title">{{ 'home.welcomeTo' | translate:{ brand: brandName } }}</h1>
|
||||
<p class="novo-hero-subtitle">{{ 'home.subtitle' | translate }}</p>
|
||||
<a [routerLink]="'/search' | langRoute" class="novo-hero-btn">
|
||||
Начать поиск
|
||||
{{ 'home.startSearch' | translate }}
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
<polyline points="12 5 19 12 12 19"></polyline>
|
||||
@@ -21,31 +21,31 @@
|
||||
@if (loading()) {
|
||||
<div class="novo-loading">
|
||||
<div class="novo-spinner"></div>
|
||||
<p>Загружаем категории...</p>
|
||||
<p>{{ 'home.loading' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (error()) {
|
||||
<div class="novo-error">
|
||||
<div class="novo-error-icon">⚠️</div>
|
||||
<h3>Что-то пошло не так</h3>
|
||||
<h3>{{ 'home.errorTitle' | translate }}</h3>
|
||||
<p>{{ error() }}</p>
|
||||
<button (click)="loadCategories()" class="novo-retry-btn">Попробовать снова</button>
|
||||
<button (click)="loadCategories()" class="novo-retry-btn">{{ 'home.retry' | translate }}</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!loading() && !error()) {
|
||||
<section class="novo-categories">
|
||||
<div class="novo-section-header">
|
||||
<h2>Категории товаров</h2>
|
||||
<p>Выберите интересующую категорию</p>
|
||||
<h2>{{ 'home.categoriesTitle' | translate }}</h2>
|
||||
<p>{{ 'home.categoriesSubtitle' | translate }}</p>
|
||||
</div>
|
||||
|
||||
@if (topLevelCategories().length === 0) {
|
||||
<div class="novo-empty">
|
||||
<div class="novo-empty-icon">📦</div>
|
||||
<h3>Категории скоро появятся</h3>
|
||||
<p>Мы работаем над наполнением каталога</p>
|
||||
<h3>{{ 'home.categoriesEmpty' | translate }}</h3>
|
||||
<p>{{ 'home.categoriesEmptyDesc' | translate }}</p>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="novo-categories-grid">
|
||||
@@ -78,16 +78,16 @@
|
||||
<section class="dexar-hero">
|
||||
<div class="dexar-hero-overlay">
|
||||
<div class="dexar-hero-content">
|
||||
<h1 class="dexar-hero-title">Здесь ты найдёшь всё</h1>
|
||||
<p class="dexar-hero-subtitle">Тысячи товаров в одном месте</p>
|
||||
<p class="dexar-hero-tagline">просто и удобно</p>
|
||||
<h1 class="dexar-hero-title">{{ 'home.dexarHeroTitle' | translate }}</h1>
|
||||
<p class="dexar-hero-subtitle">{{ 'home.dexarHeroSubtitle' | translate }}</p>
|
||||
<p class="dexar-hero-tagline">{{ 'home.dexarHeroTagline' | translate }}</p>
|
||||
|
||||
<div class="dexar-hero-actions">
|
||||
<a (click)="scrollToCatalog()" class="dexar-btn-primary">
|
||||
Перейти в каталог
|
||||
{{ 'home.goToCatalog' | translate }}
|
||||
</a>
|
||||
<button (click)="navigateToSearch()" class="dexar-btn-secondary">
|
||||
Найти товар
|
||||
{{ 'home.findProduct' | translate }}
|
||||
<svg width="11" height="16" viewBox="0 0 11 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 1L9 8L1 15" stroke="#1E3C38" stroke-width="2"/>
|
||||
</svg>
|
||||
@@ -103,25 +103,25 @@
|
||||
@if (loading()) {
|
||||
<div class="dexar-loading">
|
||||
<div class="dexar-spinner"></div>
|
||||
<p>Загрузка категорий...</p>
|
||||
<p>{{ 'home.loadingDexar' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (error()) {
|
||||
<div class="dexar-error">
|
||||
<p>{{ error() }}</p>
|
||||
<button (click)="loadCategories()" class="dexar-retry-btn">Попробовать снова</button>
|
||||
<button (click)="loadCategories()" class="dexar-retry-btn">{{ 'home.retry' | translate }}</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!loading() && !error()) {
|
||||
<section class="dexar-categories" id="catalog">
|
||||
<h2 class="dexar-categories-title">Каталог товаров</h2>
|
||||
<h2 class="dexar-categories-title">{{ 'home.catalogTitle' | translate }}</h2>
|
||||
@if (topLevelCategories().length === 0) {
|
||||
<div class="dexar-empty-categories">
|
||||
<div class="dexar-empty-icon">📦</div>
|
||||
<h3>Категории пока отсутствуют</h3>
|
||||
<p>Скоро здесь появятся категории товаров</p>
|
||||
<h3>{{ 'home.emptyCategoriesDexar' | translate }}</h3>
|
||||
<p>{{ 'home.categoriesSoonDexar' | translate }}</p>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="dexar-categories-grid">
|
||||
@@ -140,7 +140,7 @@
|
||||
</div>
|
||||
<div class="dexar-category-info">
|
||||
<h3 class="dexar-category-name">{{ category.name }}</h3>
|
||||
<p class="dexar-category-count">{{ getItemCount(category.categoryID) }} товаров</p>
|
||||
<p class="dexar-category-count">{{ 'home.itemsCount' | translate:{ count: getItemCount(category.categoryID) } }}</p>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ import { Category } from '../../models';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { ItemsCarouselComponent } from '../../components/items-carousel/items-carousel.component';
|
||||
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
|
||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
imports: [RouterLink, ItemsCarouselComponent, LangRoutePipe],
|
||||
imports: [RouterLink, ItemsCarouselComponent, LangRoutePipe, TranslatePipe],
|
||||
templateUrl: './home.component.html',
|
||||
styleUrls: ['./home.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
@if (loading()) {
|
||||
<div class="novo-loading">
|
||||
<div class="novo-spinner"></div>
|
||||
<p>Загрузка...</p>
|
||||
<p>{{ 'itemDetail.loading' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (error()) {
|
||||
<div class="novo-error">
|
||||
<p>{{ error() }}</p>
|
||||
<a [routerLink]="'/' | langRoute" class="novo-back-link">Вернуться</a>
|
||||
<a [routerLink]="'/' | langRoute" class="novo-back-link">{{ 'itemDetail.back' | translate }}</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||
<path d="M21 15l-5-5L5 21"></path>
|
||||
</svg>
|
||||
<p>Нет изображения</p>
|
||||
<p>{{ 'itemDetail.noImage' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -76,10 +76,10 @@
|
||||
</div>
|
||||
|
||||
<div class="novo-stock">
|
||||
<span class="stock-label">Наличие:</span>
|
||||
<span class="stock-label">{{ 'itemDetail.stock' | translate }}</span>
|
||||
<div class="stock-indicator" [class.high]="item()!.remainings === 'high'" [class.medium]="item()!.remainings === 'medium'" [class.low]="item()!.remainings === 'low'">
|
||||
<span class="dot"></span>
|
||||
{{ item()!.remainings === 'high' ? 'В наличии' : item()!.remainings === 'medium' ? 'Мало' : 'Осталось немного' }}
|
||||
{{ item()!.remainings === 'high' ? ('itemDetail.inStock' | translate) : item()!.remainings === 'medium' ? ('itemDetail.mediumStock' | translate) : ('itemDetail.lowStock' | translate) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -89,24 +89,24 @@
|
||||
<circle cx="20" cy="21" r="1"></circle>
|
||||
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
|
||||
</svg>
|
||||
Добавить в корзину
|
||||
{{ 'itemDetail.addToCart' | translate }}
|
||||
</button>
|
||||
|
||||
<div class="novo-description">
|
||||
<h3>Описание</h3>
|
||||
<h3>{{ 'itemDetail.description' | translate }}</h3>
|
||||
<div [innerHTML]="getSafeHtml(item()!.description)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="novo-reviews">
|
||||
<h2>Отзывы ({{ item()!.callbacks?.length || 0 }})</h2>
|
||||
<h2>{{ 'itemDetail.reviews' | translate }} ({{ item()!.callbacks?.length || 0 }})</h2>
|
||||
|
||||
<!-- novo Review Form -->
|
||||
<div class="novo-review-form">
|
||||
<h3>Ваш отзыв</h3>
|
||||
<h3>{{ 'itemDetail.yourReview' | translate }}</h3>
|
||||
<div class="novo-rating-input">
|
||||
<label>Оценка:</label>
|
||||
<label>{{ 'itemDetail.rating' | translate }}</label>
|
||||
<div class="novo-star-selector">
|
||||
@for (star of [1, 2, 3, 4, 5]; track star) {
|
||||
<span
|
||||
@@ -120,14 +120,14 @@
|
||||
</div>
|
||||
<textarea
|
||||
[(ngModel)]="newReview.comment"
|
||||
placeholder="Поделитесь своими впечатлениями о товаре..."
|
||||
[placeholder]="'itemDetail.reviewPlaceholder' | translate"
|
||||
rows="4"
|
||||
class="novo-textarea">
|
||||
</textarea>
|
||||
<div class="novo-form-actions">
|
||||
<label class="novo-anonymous-toggle">
|
||||
<input type="checkbox" [(ngModel)]="newReview.anonymous">
|
||||
<span>Анонимно</span>
|
||||
<span>{{ 'itemDetail.anonymous' | translate }}</span>
|
||||
</label>
|
||||
@if (!newReview.anonymous && getUserDisplayName()) {
|
||||
<span class="novo-username-preview">{{ getUserDisplayName() }}</span>
|
||||
@@ -139,12 +139,12 @@
|
||||
[class.submitting]="reviewSubmitStatus() === 'loading'">
|
||||
@if (reviewSubmitStatus() === 'loading') {
|
||||
<span class="novo-spinner-small"></span>
|
||||
Отправка...
|
||||
{{ 'itemDetail.submitting' | translate }}
|
||||
} @else {
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/>
|
||||
</svg>
|
||||
Отправить
|
||||
{{ 'itemDetail.submit' | translate }}
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
@@ -154,7 +154,7 @@
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<path d="M20 6L9 17l-5-5"/>
|
||||
</svg>
|
||||
Спасибо за ваш отзыв!
|
||||
{{ 'itemDetail.reviewSuccess' | translate }}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
Ошибка отправки. Попробуйте позже.
|
||||
{{ 'itemDetail.reviewError' | translate }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -174,7 +174,7 @@
|
||||
<div class="novo-review-card">
|
||||
<div class="review-header">
|
||||
<div class="reviewer-info">
|
||||
<span class="reviewer-name">{{ review.userID || 'Пользователь' }}</span>
|
||||
<span class="reviewer-name">{{ review.userID ? review.userID : ('itemDetail.defaultUser' | translate) }}</span>
|
||||
@if (review.timestamp) {
|
||||
<span class="review-date">{{ formatDate(review.timestamp) }}</span>
|
||||
}
|
||||
@@ -185,7 +185,7 @@
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<p class="novo-no-reviews">Пока нет отзывов. Станьте первым!</p>
|
||||
<p class="novo-no-reviews">{{ 'itemDetail.noReviews' | translate }}</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -197,14 +197,14 @@
|
||||
@if (loading()) {
|
||||
<div class="dx-loading">
|
||||
<div class="dx-spinner"></div>
|
||||
<p>Загрузка товара...</p>
|
||||
<p>{{ 'itemDetail.loadingDexar' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (error()) {
|
||||
<div class="dx-error">
|
||||
<p>{{ error() }}</p>
|
||||
<a [routerLink]="'/' | langRoute" class="dx-back-link">Вернуться на главную</a>
|
||||
<a [routerLink]="'/' | langRoute" class="dx-back-link">{{ 'itemDetail.backHome' | translate }}</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@
|
||||
@if (photo.video) {
|
||||
<div class="dx-video-badge">▶</div>
|
||||
}
|
||||
<img [src]="photo.url" [alt]="'Фото ' + ($index + 1)" loading="lazy" decoding="async" />
|
||||
<img [src]="photo.url" [alt]="('itemDetail.photo' | translate) + ' ' + ($index + 1)" loading="lazy" decoding="async" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -241,7 +241,7 @@
|
||||
<circle cx="8.5" cy="8.5" r="1.5"></circle>
|
||||
<path d="M21 15l-5-5L5 21"></path>
|
||||
</svg>
|
||||
<span>Нет изображения</span>
|
||||
<span>{{ 'itemDetail.noImage' | translate }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -260,7 +260,7 @@
|
||||
}
|
||||
</div>
|
||||
<span class="dx-rating-value">{{ item()!.rating }}</span>
|
||||
<span class="dx-rating-count">({{ item()!.callbacks?.length || 0 }} отзывов)</span>
|
||||
<span class="dx-rating-count">({{ item()!.callbacks?.length || 0 }} {{ 'itemDetail.reviewsCount' | translate }})</span>
|
||||
</div>
|
||||
|
||||
<div class="dx-price-block">
|
||||
@@ -276,13 +276,13 @@
|
||||
</div>
|
||||
|
||||
<div class="dx-stock">
|
||||
<span class="dx-stock-label">Наличие:</span>
|
||||
<span class="dx-stock-label">{{ 'itemDetail.stock' | translate }}</span>
|
||||
<span class="dx-stock-status"
|
||||
[class.high]="item()!.remainings === 'high'"
|
||||
[class.medium]="item()!.remainings === 'medium'"
|
||||
[class.low]="item()!.remainings === 'low'">
|
||||
<span class="dx-stock-dot"></span>
|
||||
{{ item()!.remainings === 'high' ? 'В наличии' : item()!.remainings === 'medium' ? 'Заканчивается' : 'Последние штуки' }}
|
||||
{{ item()!.remainings === 'high' ? ('itemDetail.inStock' | translate) : item()!.remainings === 'medium' ? ('itemDetail.mediumStock' | translate) : ('itemDetail.lastItems' | translate) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -292,11 +292,11 @@
|
||||
<circle cx="20" cy="21" r="1"></circle>
|
||||
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
|
||||
</svg>
|
||||
Добавить в корзину
|
||||
{{ 'itemDetail.addToCart' | translate }}
|
||||
</button>
|
||||
|
||||
<div class="dx-description">
|
||||
<h2>Описание</h2>
|
||||
<h2>{{ 'itemDetail.description' | translate }}</h2>
|
||||
<div class="dx-description-text" [innerHTML]="getSafeHtml(item()!.description)"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,12 +304,12 @@
|
||||
|
||||
<!-- Reviews Section -->
|
||||
<div class="dx-reviews-section">
|
||||
<h2>Отзывы ({{ item()!.callbacks?.length || 0 }})</h2>
|
||||
<h2>{{ 'itemDetail.reviews' | translate }} ({{ item()!.callbacks?.length || 0 }})</h2>
|
||||
|
||||
<div class="dx-review-form">
|
||||
<h3>Оставить отзыв</h3>
|
||||
<h3>{{ 'itemDetail.leaveReview' | translate }}</h3>
|
||||
<div class="dx-rating-input">
|
||||
<label>Оценка:</label>
|
||||
<label>{{ 'itemDetail.rating' | translate }}</label>
|
||||
<div class="dx-star-selector">
|
||||
@for (star of [1, 2, 3, 4, 5]; track star) {
|
||||
<span
|
||||
@@ -323,14 +323,14 @@
|
||||
</div>
|
||||
<textarea
|
||||
[(ngModel)]="newReview.comment"
|
||||
placeholder="Поделитесь впечатлениями о товаре..."
|
||||
[placeholder]="'itemDetail.reviewPlaceholderDexar' | translate"
|
||||
rows="4"
|
||||
class="dx-textarea">
|
||||
</textarea>
|
||||
<div class="dx-form-actions">
|
||||
<label class="dx-anon-toggle">
|
||||
<input type="checkbox" [(ngModel)]="newReview.anonymous">
|
||||
<span>Анонимно</span>
|
||||
<span>{{ 'itemDetail.anonymous' | translate }}</span>
|
||||
</label>
|
||||
@if (!newReview.anonymous && getUserDisplayName()) {
|
||||
<span class="dx-user-preview">{{ getUserDisplayName() }}</span>
|
||||
@@ -342,9 +342,9 @@
|
||||
[class.submitting]="reviewSubmitStatus() === 'loading'">
|
||||
@if (reviewSubmitStatus() === 'loading') {
|
||||
<span class="dx-spinner-sm"></span>
|
||||
Отправка...
|
||||
{{ 'itemDetail.submitting' | translate }}
|
||||
} @else {
|
||||
Отправить
|
||||
{{ 'itemDetail.submit' | translate }}
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
@@ -352,14 +352,14 @@
|
||||
@if (reviewSubmitStatus() === 'success') {
|
||||
<div class="dx-status success">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg>
|
||||
Спасибо за ваш отзыв!
|
||||
{{ 'itemDetail.reviewSuccess' | translate }}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (reviewSubmitStatus() === 'error') {
|
||||
<div class="dx-status error">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
Ошибка отправки. Попробуйте позже.
|
||||
{{ 'itemDetail.reviewError' | translate }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -370,7 +370,7 @@
|
||||
<div class="dx-review-card">
|
||||
<div class="dx-review-header">
|
||||
<div class="dx-reviewer">
|
||||
<span class="dx-reviewer-name">{{ callback.userID || 'Аноним' }}</span>
|
||||
<span class="dx-reviewer-name">{{ callback.userID ? callback.userID : ('itemDetail.defaultUser' | translate) }}</span>
|
||||
@if (callback.timestamp) {
|
||||
<span class="dx-review-date">{{ formatDate(callback.timestamp) }}</span>
|
||||
}
|
||||
@@ -391,7 +391,7 @@
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<p class="dx-no-reviews">Пока нет отзывов. Станьте первым!</p>
|
||||
<p class="dx-no-reviews">{{ 'itemDetail.noReviews' | translate }}</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -399,7 +399,7 @@
|
||||
<!-- Q&A Section -->
|
||||
@if (item()!.questions && item()!.questions!.length > 0) {
|
||||
<div class="dx-qa-section">
|
||||
<h2>Вопросы и ответы ({{ item()!.questions!.length }})</h2>
|
||||
<h2>{{ 'itemDetail.qna' | translate }} ({{ item()!.questions!.length }})</h2>
|
||||
<div class="dx-qa-list">
|
||||
@for (question of item()!.questions!; track $index) {
|
||||
<div class="dx-qa-card">
|
||||
|
||||
@@ -10,10 +10,12 @@ import { environment } from '../../../environments/environment';
|
||||
import { SecurityContext } from '@angular/core';
|
||||
import { getDiscountedPrice } from '../../utils/item.utils';
|
||||
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
|
||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||
import { TranslateService } from '../../i18n/translate.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-item-detail',
|
||||
imports: [DecimalPipe, RouterLink, FormsModule, LangRoutePipe],
|
||||
imports: [DecimalPipe, RouterLink, FormsModule, LangRoutePipe, TranslatePipe],
|
||||
templateUrl: './item-detail.component.html',
|
||||
styleUrls: ['./item-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
@@ -39,6 +41,7 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
|
||||
private reloadTimeout?: ReturnType<typeof setTimeout>;
|
||||
|
||||
private seoService = inject(SeoService);
|
||||
private i18n = inject(TranslateService);
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
@@ -115,10 +118,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 this.i18n.t('itemDetail.today');
|
||||
if (diffDays === 1) return this.i18n.t('itemDetail.yesterday');
|
||||
if (diffDays < 7) return `${diffDays} ${this.i18n.t('itemDetail.daysAgo')}`;
|
||||
if (diffDays < 30) return `${Math.floor(diffDays / 7)} ${this.i18n.t('itemDetail.weeksAgo')}`;
|
||||
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
@@ -133,7 +136,7 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
getUserDisplayName(): string | null {
|
||||
if (!this.telegramService.isTelegramApp()) {
|
||||
return 'Пользователь';
|
||||
return this.i18n.t('itemDetail.defaultUser');
|
||||
}
|
||||
return this.telegramService.getDisplayName();
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<div class="search-container">
|
||||
<div class="search-header">
|
||||
<h1>Поиск товаров</h1>
|
||||
<h1>{{ 'search.title' | translate }}</h1>
|
||||
<div class="search-box">
|
||||
<input
|
||||
type="text"
|
||||
[(ngModel)]="searchQuery"
|
||||
(input)="onSearchInput(searchQuery)"
|
||||
placeholder="Введите название товара..."
|
||||
[placeholder]="'search.placeholder' | translate"
|
||||
class="search-input"
|
||||
autofocus
|
||||
/>
|
||||
@@ -21,21 +21,21 @@
|
||||
|
||||
@if (searchQuery && items().length > 0) {
|
||||
<div class="results-count">
|
||||
Найдено товаров: {{ items().length }}@if (totalResults() > items().length) { из {{ totalResults() }} }
|
||||
{{ 'search.resultsCount' | translate }} {{ items().length }}@if (totalResults() > items().length) { {{ 'search.of' | translate }} {{ totalResults() }} }
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (loading() && items().length === 0) {
|
||||
<div class="loading-initial">
|
||||
<div class="spinner"></div>
|
||||
<p>Поиск...</p>
|
||||
<p>{{ 'search.searching' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (error()) {
|
||||
<div class="error">
|
||||
<p>{{ error() }}</p>
|
||||
<button (click)="performSearch(searchQuery)">Попробовать снова</button>
|
||||
<button (click)="performSearch(searchQuery)">{{ 'search.retry' | translate }}</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -47,9 +47,9 @@
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.2929 18.2929C18.6834 17.9024 19.3166 17.9024 19.7071 18.2929L25.7071 24.2929C26.0976 24.6834 26.0976 25.3166 25.7071 25.7071C25.3166 26.0976 24.6834 26.0976 24.2929 25.7071L18.2929 19.7071C17.9024 19.3166 17.9024 18.6834 18.2929 18.2929Z" fill="#a1b4b5" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2>Ничего не найдено</h2>
|
||||
<p>По запросу "{{ searchQuery }}" товары не найдены</p>
|
||||
<p class="hint">Попробуйте изменить запрос или используйте другие ключевые слова</p>
|
||||
<h2>{{ 'search.noResults' | translate }}</h2>
|
||||
<p>{{ 'search.noResultsFor' | translate:{ query: searchQuery } }}</p>
|
||||
<p class="hint">{{ 'search.noResultsHint' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
</a>
|
||||
|
||||
<button class="add-to-cart-btn" (click)="addToCart(item.itemID, $event)">
|
||||
В корзину
|
||||
{{ 'search.addToCart' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@@ -104,13 +104,13 @@
|
||||
@if (loading() && items().length > 0) {
|
||||
<div class="loading-more">
|
||||
<div class="spinner"></div>
|
||||
<p>Загрузка...</p>
|
||||
<p>{{ 'search.loadingMore' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!hasMore() && items().length > 0) {
|
||||
<div class="no-more">
|
||||
<p>Все результаты загружены</p>
|
||||
<p>{{ 'search.allLoaded' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -123,7 +123,7 @@
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.2929 18.2929C18.6834 17.9024 19.3166 17.9024 19.7071 18.2929L25.7071 24.2929C26.0976 24.6834 26.0976 25.3166 25.7071 25.7071C25.3166 26.0976 24.6834 26.0976 24.2929 25.7071L18.2929 19.7071C17.9024 19.3166 17.9024 18.6834 18.2929 18.2929Z" fill="#d3dad9" />
|
||||
</svg>
|
||||
</div>
|
||||
<p>Введите запрос для поиска товаров</p>
|
||||
<p>{{ 'search.emptyState' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, signal, HostListener, OnDestroy, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Component, signal, HostListener, OnDestroy, ChangeDetectionStrategy, inject } from '@angular/core';
|
||||
import { DecimalPipe } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { RouterLink } from '@angular/router';
|
||||
@@ -8,10 +8,12 @@ import { Subject, Subscription } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||
import { getDiscountedPrice, getMainImage, trackByItemId } from '../../utils/item.utils';
|
||||
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
|
||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||
import { TranslateService } from '../../i18n/translate.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-search',
|
||||
imports: [DecimalPipe, FormsModule, RouterLink, LangRoutePipe],
|
||||
imports: [DecimalPipe, FormsModule, RouterLink, LangRoutePipe, TranslatePipe],
|
||||
templateUrl: './search.component.html',
|
||||
styleUrls: ['./search.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
@@ -29,6 +31,7 @@ export class SearchComponent implements OnDestroy {
|
||||
private isLoadingMore = false;
|
||||
private searchSubject = new Subject<string>();
|
||||
private searchSubscription: Subscription;
|
||||
private i18n = inject(TranslateService);
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
@@ -100,7 +103,7 @@ export class SearchComponent implements OnDestroy {
|
||||
this.isLoadingMore = false;
|
||||
},
|
||||
error: (err) => {
|
||||
this.error.set('Ошибка при поиске товаров');
|
||||
this.error.set(this.i18n.t('home.errorTitle'));
|
||||
this.loading.set(false);
|
||||
this.isLoadingMore = false;
|
||||
console.error('Error searching items:', err);
|
||||
|
||||
Reference in New Issue
Block a user