Merge remote-tracking branch 'origin' into back-office-integration
This commit is contained in:
@@ -1,17 +1,21 @@
|
||||
import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy, inject } from '@angular/core';
|
||||
import { DecimalPipe } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
import { ApiService, CartService, TelegramService, LanguageService } from '../../services';
|
||||
import { ApiService, CartService, TelegramService, LanguageService, SeoService } from '../../services';
|
||||
import { Item, DescriptionField } from '../../models';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { getAllImages, getStockStatus, getBadgeClass, getTranslatedField } from '../../utils/item.utils';
|
||||
import { SecurityContext } from '@angular/core';
|
||||
import { getDiscountedPrice, getAllImages, getStockStatus, getBadgeClass, getTranslatedField } 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],
|
||||
imports: [DecimalPipe, RouterLink, FormsModule, LangRoutePipe, TranslatePipe],
|
||||
templateUrl: './item-detail.component.html',
|
||||
styleUrls: ['./item-detail.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
@@ -32,6 +36,12 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
|
||||
reviewSubmitStatus = signal<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||
|
||||
private routeSubscription?: Subscription;
|
||||
private reviewResetTimeout?: ReturnType<typeof setTimeout>;
|
||||
private reviewErrorTimeout?: ReturnType<typeof setTimeout>;
|
||||
private reloadTimeout?: ReturnType<typeof setTimeout>;
|
||||
|
||||
private seoService = inject(SeoService);
|
||||
private i18n = inject(TranslateService);
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
@@ -51,6 +61,10 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.routeSubscription?.unsubscribe();
|
||||
if (this.reviewResetTimeout) clearTimeout(this.reviewResetTimeout);
|
||||
if (this.reviewErrorTimeout) clearTimeout(this.reviewErrorTimeout);
|
||||
if (this.reloadTimeout) clearTimeout(this.reloadTimeout);
|
||||
this.seoService.resetToDefaults();
|
||||
}
|
||||
|
||||
loadItem(itemID: number): void {
|
||||
@@ -59,6 +73,7 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
|
||||
this.apiService.getItem(itemID).subscribe({
|
||||
next: (item) => {
|
||||
this.item.set(item);
|
||||
this.seoService.setItemMeta(item);
|
||||
this.loading.set(false);
|
||||
},
|
||||
error: (err) => {
|
||||
@@ -83,7 +98,7 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
|
||||
getDiscountedPrice(): number {
|
||||
const currentItem = this.item();
|
||||
if (!currentItem) return 0;
|
||||
return currentItem.price * (1 - (currentItem.discount || 0) / 100);
|
||||
return getDiscountedPrice(currentItem);
|
||||
}
|
||||
|
||||
// BackOffice integration helpers
|
||||
@@ -138,14 +153,14 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
|
||||
readonly getBadgeClass = getBadgeClass;
|
||||
|
||||
getSafeHtml(html: string): SafeHtml {
|
||||
return this.sanitizer.sanitize(1, html) || '';
|
||||
return this.sanitizer.sanitize(SecurityContext.HTML, html) || '';
|
||||
}
|
||||
|
||||
getRatingStars(rating: number): string {
|
||||
const fullStars = Math.floor(rating);
|
||||
const hasHalfStar = rating % 1 >= 0.5;
|
||||
let stars = '?'.repeat(fullStars);
|
||||
if (hasHalfStar) stars += '?';
|
||||
let stars = '★'.repeat(fullStars);
|
||||
if (hasHalfStar) stars += '☆';
|
||||
return stars;
|
||||
}
|
||||
|
||||
@@ -155,10 +170,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 '<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>';
|
||||
if (diffDays === 1) return '<27><><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>`;
|
||||
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',
|
||||
@@ -173,7 +188,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();
|
||||
}
|
||||
@@ -202,13 +217,13 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
|
||||
this.reviewSubmitStatus.set('success');
|
||||
this.newReview = { rating: 0, comment: '', anonymous: false };
|
||||
|
||||
// <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(() => {
|
||||
// Сброс состояния через 3 секунды
|
||||
this.reviewResetTimeout = 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.reloadTimeout = setTimeout(() => {
|
||||
this.loadItem(currentItem.itemID);
|
||||
}, 500);
|
||||
},
|
||||
@@ -216,8 +231,8 @@ export class ItemDetailComponent implements OnInit, OnDestroy {
|
||||
console.error('Error submitting review:', err);
|
||||
this.reviewSubmitStatus.set('error');
|
||||
|
||||
// <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(() => {
|
||||
// Сброс состояния об ошибке через 5 секунд
|
||||
this.reviewErrorTimeout = setTimeout(() => {
|
||||
this.reviewSubmitStatus.set('idle');
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user