Merge remote-tracking branch 'origin' into back-office-integration
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="/" 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>
|
||||
}
|
||||
|
||||
@@ -30,13 +30,13 @@
|
||||
[class.swiped]="swipedItemId() === item.itemID"
|
||||
(touchstart)="onSwipeStart(item.itemID, $event)">
|
||||
<div class="cart-item">
|
||||
<a [routerLink]="['/item', item.itemID]" class="item-image">
|
||||
<a [routerLink]="['/item', item.itemID] | langRoute" class="item-image">
|
||||
<img [src]="getMainImage(item)" [alt]="item.name" loading="lazy" />
|
||||
</a>
|
||||
|
||||
<div class="item-info">
|
||||
<div class="item-header">
|
||||
<a [routerLink]="['/item', item.itemID]" class="item-name">{{ item.name }}</a>
|
||||
<a [routerLink]="['/item', item.itemID] | langRoute" class="item-name">{{ item.name }}</a>
|
||||
<button class="remove-btn" (click)="removeItem(item.itemID)" title="Remove">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
@@ -94,21 +94,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>
|
||||
|
||||
@@ -121,11 +121,11 @@
|
||||
/>
|
||||
<span class="checkmark"></span>
|
||||
<span class="terms-text">
|
||||
Я согласен с
|
||||
<a routerLink="/public-offer" target="_blank">публичной офертой</a>,
|
||||
<a routerLink="/return-policy" target="_blank">политикой возврата</a>,
|
||||
<a routerLink="/guarantee" target="_blank">условиями гарантии</a> и
|
||||
<a routerLink="/privacy-policy" 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>
|
||||
@@ -136,7 +136,7 @@
|
||||
[class.disabled]="!termsAccepted"
|
||||
[disabled]="!termsAccepted"
|
||||
>
|
||||
Оформить заказ
|
||||
{{ 'cart.checkout' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -147,7 +147,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>
|
||||
@@ -155,14 +155,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">
|
||||
@@ -173,22 +173,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>
|
||||
@@ -197,8 +197,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">
|
||||
@@ -244,9 +244,9 @@
|
||||
>
|
||||
@if (emailSubmitting()) {
|
||||
<span class="spinner-small"></span>
|
||||
Отправка...
|
||||
{{ 'cart.sending' | translate }}
|
||||
} @else {
|
||||
Отправить
|
||||
{{ 'cart.send' | translate }}
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
@@ -256,9 +256,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,29 +1,34 @@
|
||||
import { Component, computed, ChangeDetectionStrategy, signal, OnDestroy, OnInit } 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';
|
||||
import { CartService, ApiService } from '../../services';
|
||||
import { CartService, ApiService, LanguageService } from '../../services';
|
||||
import { Item, CartItem } from '../../models';
|
||||
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, getBadgeClass } 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],
|
||||
imports: [DecimalPipe, RouterLink, FormsModule, EmptyCartIconComponent, LangRoutePipe, TranslatePipe],
|
||||
templateUrl: './cart.component.html',
|
||||
styleUrls: ['./cart.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CartComponent implements OnInit, OnDestroy {
|
||||
export class CartComponent implements OnDestroy {
|
||||
items;
|
||||
itemCount;
|
||||
totalPrice;
|
||||
termsAccepted = false;
|
||||
isnovo = environment.theme === 'novo';
|
||||
|
||||
private i18n = inject(TranslateService);
|
||||
|
||||
// Swipe state
|
||||
swipedItemId = signal<number | null>(null);
|
||||
|
||||
@@ -52,17 +57,14 @@ export class CartComponent implements OnInit, OnDestroy {
|
||||
constructor(
|
||||
private cartService: CartService,
|
||||
private apiService: ApiService,
|
||||
private router: Router
|
||||
private router: Router,
|
||||
private langService: LanguageService
|
||||
) {
|
||||
this.items = this.cartService.items;
|
||||
this.itemCount = this.cartService.itemCount;
|
||||
this.totalPrice = this.cartService.totalPrice;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Component initialized
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.stopPolling();
|
||||
if (this.closeTimeout) {
|
||||
@@ -109,16 +111,16 @@ export class CartComponent implements OnInit, OnDestroy {
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
document.removeEventListener('touchmove', onMove as any);
|
||||
document.removeEventListener('touchmove', onMove);
|
||||
document.removeEventListener('touchend', cleanup);
|
||||
};
|
||||
|
||||
document.addEventListener('touchmove', onMove as any);
|
||||
document.addEventListener('touchmove', onMove);
|
||||
document.addEventListener('touchend', cleanup);
|
||||
}
|
||||
|
||||
clearCart(): void {
|
||||
if (confirm('<27><> <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>?')) {
|
||||
if (confirm(this.i18n.t('cart.confirmClear'))) {
|
||||
this.cartService.clearCart();
|
||||
}
|
||||
}
|
||||
@@ -130,7 +132,7 @@ export class CartComponent implements OnInit, OnDestroy {
|
||||
|
||||
checkout(): void {
|
||||
if (!this.termsAccepted) {
|
||||
alert('<27><><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>.');
|
||||
alert(this.i18n.t('cart.acceptTerms'));
|
||||
return;
|
||||
}
|
||||
this.openPaymentPopup();
|
||||
@@ -252,7 +254,7 @@ export class CartComponent implements OnInit, OnDestroy {
|
||||
this.linkCopied.set(true);
|
||||
setTimeout(() => this.linkCopied.set(false), 2000);
|
||||
}).catch(err => {
|
||||
console.error('<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>:', err);
|
||||
console.error(this.i18n.t('cart.copyError'), err);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -317,17 +319,18 @@ export class CartComponent implements OnInit, OnDestroy {
|
||||
next: () => {
|
||||
this.emailSubmitting.set(false);
|
||||
// Show success message
|
||||
alert('Email <20><><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>.');
|
||||
alert(this.i18n.t('cart.emailSuccess'));
|
||||
// Close popup and redirect to home page
|
||||
setTimeout(() => {
|
||||
this.closePaymentPopup();
|
||||
this.router.navigate(['/']);
|
||||
const lang = this.langService.currentLanguage();
|
||||
this.router.navigate([`/${lang}`]);
|
||||
}, 500);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error submitting email:', err);
|
||||
this.emailSubmitting.set(false);
|
||||
alert('<27><><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>.');
|
||||
alert(this.i18n.t('cart.emailError'));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -388,11 +391,11 @@ export class CartComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
if (digitsOnly.length === 0) {
|
||||
this.phoneError.set('<27><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>');
|
||||
this.phoneError.set(this.i18n.t('cart.phoneRequired'));
|
||||
} else if (digitsOnly.length < 11) {
|
||||
this.phoneError.set(`<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> ${11 - digitsOnly.length} <20><><EFBFBD><EFBFBD>`);
|
||||
this.phoneError.set(this.i18n.t('cart.phoneMoreDigits', { count: 11 - digitsOnly.length }));
|
||||
} else if (digitsOnly.length > 11) {
|
||||
this.phoneError.set('<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>');
|
||||
this.phoneError.set(this.i18n.t('cart.phoneTooMany'));
|
||||
} else {
|
||||
this.phoneError.set('');
|
||||
}
|
||||
@@ -420,19 +423,19 @@ export class CartComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
if (email.length === 0) {
|
||||
this.emailError.set('Email <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>');
|
||||
this.emailError.set(this.i18n.t('cart.emailRequired'));
|
||||
} else if (email.length < 5) {
|
||||
this.emailError.set('Email <20><><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>)');
|
||||
this.emailError.set(this.i18n.t('cart.emailTooShort'));
|
||||
} else if (email.length > 100) {
|
||||
this.emailError.set('Email <20><><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>)');
|
||||
this.emailError.set(this.i18n.t('cart.emailTooLong'));
|
||||
} else if (!email.includes('@')) {
|
||||
this.emailError.set('Email <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> @');
|
||||
this.emailError.set(this.i18n.t('cart.emailNeedsAt'));
|
||||
} else if (!email.includes('.')) {
|
||||
this.emailError.set('Email <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> (.com, .ru <20> <20>.<2E>.)');
|
||||
this.emailError.set(this.i18n.t('cart.emailNeedsDomain'));
|
||||
} else {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
this.emailError.set('<27><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> email');
|
||||
this.emailError.set(this.i18n.t('cart.emailInvalid'));
|
||||
} else {
|
||||
this.emailError.set('');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user