very first commit
This commit is contained in:
258
src/app/pages/cart/cart.component.html
Normal file
258
src/app/pages/cart/cart.component.html
Normal file
@@ -0,0 +1,258 @@
|
||||
<div [class]="isnovo ? 'cart-container novo' : 'cart-container dexar'">
|
||||
<div class="cart-header">
|
||||
<h1>Корзина</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>
|
||||
Очистить
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (itemCount() === 0) {
|
||||
<div class="empty-cart">
|
||||
<div class="empty-icon">
|
||||
<app-empty-cart-icon />
|
||||
</div>
|
||||
<h2>Корзина пуста</h2>
|
||||
<p>Добавьте товары, чтобы начать покупки</p>
|
||||
<a routerLink="/" class="shop-btn">Перейти к покупкам</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (itemCount() > 0) {
|
||||
<div class="cart-content">
|
||||
<div class="cart-items">
|
||||
@for (item of items(); track trackByItemId($index, item)) {
|
||||
<div class="cart-item-wrapper"
|
||||
[class.swiped]="swipedItemId() === item.itemID"
|
||||
(touchstart)="onSwipeStart(item.itemID, $event)">
|
||||
<div class="cart-item">
|
||||
<a [routerLink]="['/item', item.itemID]" 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>
|
||||
<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"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="item-description">{{ item.description.substring(0, 100) }}...</p>
|
||||
|
||||
<div class="item-footer">
|
||||
<div class="item-pricing">
|
||||
@if (item.discount > 0) {
|
||||
<div class="price-with-discount">
|
||||
<span class="original-price">{{ item.price }} ₽</span>
|
||||
<span class="current-price">{{ getDiscountedPrice(item) | number:'1.2-2' }} ₽</span>
|
||||
</div>
|
||||
} @else {
|
||||
<span class="current-price">{{ item.price }} ₽</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="quantity-controls">
|
||||
<button class="qty-btn" (click)="decreaseQuantity(item.itemID, item.quantity)" [disabled]="item.quantity <= 1">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<path d="M5 12h14"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="qty-value">{{ item.quantity }}</span>
|
||||
<button class="qty-btn" (click)="increaseQuantity(item.itemID, item.quantity)">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<path d="M12 5v14M5 12h14"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="delete-btn-mobile" (click)="removeItem(item.itemID)">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M3 6h18M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2" stroke="white" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="cart-summary">
|
||||
<div class="summary-header">
|
||||
<h3>Итого</h3>
|
||||
</div>
|
||||
|
||||
<div class="summary-row">
|
||||
<span>Товары ({{ itemCount() }})</span>
|
||||
<span class="value">{{ totalPrice() | number:'1.2-2' }} ₽</span>
|
||||
</div>
|
||||
|
||||
<div class="summary-row delivery">
|
||||
<span>Доставка</span>
|
||||
<span>0 ₽</span>
|
||||
</div>
|
||||
|
||||
<div class="summary-row total">
|
||||
<span>К оплате</span>
|
||||
<span class="total-price">{{ totalPrice() | number:'1.2-2' }} ₽</span>
|
||||
</div>
|
||||
|
||||
<div class="terms-agreement">
|
||||
<label class="checkbox-container">
|
||||
<input
|
||||
type="checkbox"
|
||||
[(ngModel)]="termsAccepted"
|
||||
id="terms-checkbox"
|
||||
/>
|
||||
<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>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="checkout-btn"
|
||||
(click)="checkout()"
|
||||
[class.disabled]="!termsAccepted"
|
||||
[disabled]="!termsAccepted"
|
||||
>
|
||||
Оформить заказ
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Payment Popup Modal -->
|
||||
@if (showPaymentPopup()) {
|
||||
<div class="payment-modal-overlay">
|
||||
<div class="payment-modal">
|
||||
<button class="close-modal-btn" (click)="closePaymentPopup()" aria-label="Закрыть">
|
||||
<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>
|
||||
</button>
|
||||
@if (paymentStatus() === 'creating') {
|
||||
<div class="payment-status-screen">
|
||||
<div class="spinner-large"></div>
|
||||
<h2>Создание платежа...</h2>
|
||||
<p>Подождите несколько секунд</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (paymentStatus() === 'waiting') {
|
||||
<div class="payment-active">
|
||||
<h2>Сканируйте QR-код для оплаты</h2>
|
||||
|
||||
<div class="qr-section">
|
||||
<div class="qr-wrapper">
|
||||
<img [src]="qrCodeUrl()" alt="QR код для оплаты" class="qr-code" />
|
||||
<div class="scan-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="payment-info">
|
||||
<div class="payment-amount">
|
||||
<span class="label">Сумма к оплате:</span>
|
||||
<span class="amount">{{ totalPrice() | number:'1.2-2' }} RUB</span>
|
||||
</div>
|
||||
|
||||
<div class="waiting-indicator">
|
||||
<div class="pulse-dot"></div>
|
||||
<span>Ожидание оплаты...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="payment-actions">
|
||||
<button class="copy-btn" (click)="copyPaymentLink()">
|
||||
{{ linkCopied() ? '✓ Скопировано' : 'Скопировать ссылку' }}
|
||||
</button>
|
||||
<a [href]="paymentUrl()" target="_blank" rel="noopener noreferrer" class="open-btn">
|
||||
Открыть в новой вкладке
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (paymentStatus() === 'success') {
|
||||
<div class="payment-status-screen success">
|
||||
<div class="success-icon">✓</div>
|
||||
<h2>Поздравляем! Оплата прошла успешно!</h2>
|
||||
<p class="success-text">Введите ваши контактные данные, и мы отправим вам покупку в течение нескольких минут</p>
|
||||
|
||||
<div class="email-form">
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="email"
|
||||
class="email-input"
|
||||
[class.valid]="emailTouched() && !emailError()"
|
||||
[class.invalid]="emailTouched() && emailError()"
|
||||
placeholder="your@email.com"
|
||||
[value]="userEmail()"
|
||||
(input)="onEmailInput($event)"
|
||||
(blur)="onEmailBlur()"
|
||||
[disabled]="emailSubmitting()"
|
||||
maxlength="100"
|
||||
/>
|
||||
@if (emailTouched() && emailError()) {
|
||||
<div class="error-message">{{ emailError() }}</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="tel"
|
||||
class="email-input phone-input"
|
||||
[class.valid]="phoneTouched() && !phoneError()"
|
||||
[class.invalid]="phoneTouched() && phoneError()"
|
||||
placeholder="+7 (900) 123-45-67"
|
||||
[value]="userPhone()"
|
||||
(input)="onPhoneInput($event)"
|
||||
(blur)="onPhoneBlur()"
|
||||
[disabled]="emailSubmitting()"
|
||||
(keyup.enter)="submitEmail()"
|
||||
/>
|
||||
@if (phoneTouched() && phoneError()) {
|
||||
<div class="error-message">{{ phoneError() }}</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="submit-email-btn"
|
||||
(click)="submitEmail()"
|
||||
[disabled]="emailSubmitting()"
|
||||
>
|
||||
@if (emailSubmitting()) {
|
||||
<span class="spinner-small"></span>
|
||||
Отправка...
|
||||
} @else {
|
||||
Отправить
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (paymentStatus() === 'timeout') {
|
||||
<div class="payment-status-screen timeout">
|
||||
<div class="timeout-icon">⏱</div>
|
||||
<h2>Время ожидания истекло</h2>
|
||||
<p>Мы не получили подтверждение оплаты в течение 3 минут.</p>
|
||||
<p class="auto-close">Окно закроется автоматически...</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
1840
src/app/pages/cart/cart.component.scss
Normal file
1840
src/app/pages/cart/cart.component.scss
Normal file
File diff suppressed because it is too large
Load Diff
449
src/app/pages/cart/cart.component.ts
Normal file
449
src/app/pages/cart/cart.component.ts
Normal file
@@ -0,0 +1,449 @@
|
||||
import { Component, computed, ChangeDetectionStrategy, signal, OnDestroy, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { CartService, ApiService } 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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-cart',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterLink, FormsModule, EmptyCartIconComponent],
|
||||
templateUrl: './cart.component.html',
|
||||
styleUrls: ['./cart.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CartComponent implements OnInit, OnDestroy {
|
||||
items;
|
||||
itemCount;
|
||||
totalPrice;
|
||||
termsAccepted = false;
|
||||
isnovo = environment.theme === 'novo';
|
||||
|
||||
// Swipe state
|
||||
swipedItemId = signal<number | null>(null);
|
||||
|
||||
// Payment popup states
|
||||
showPaymentPopup = signal<boolean>(false);
|
||||
paymentStatus = signal<'creating' | 'waiting' | 'success' | 'timeout'>('creating');
|
||||
qrCodeUrl = signal<string>('');
|
||||
paymentUrl = signal<string>('');
|
||||
paymentId = signal<string>('');
|
||||
linkCopied = signal<boolean>(false);
|
||||
|
||||
// Email collection after successful payment
|
||||
userEmail = signal<string>('');
|
||||
userPhone = signal<string>('');
|
||||
emailTouched = signal<boolean>(false);
|
||||
phoneTouched = signal<boolean>(false);
|
||||
emailError = signal<string>('');
|
||||
phoneError = signal<string>('');
|
||||
emailSubmitting = signal<boolean>(false);
|
||||
paidItems: CartItem[] = [];
|
||||
|
||||
maxChecks = 36; // 36 checks * 5 seconds = 180 seconds (3 minutes)
|
||||
private pollingSubscription?: Subscription;
|
||||
private closeTimeout?: ReturnType<typeof setTimeout>;
|
||||
|
||||
constructor(
|
||||
private cartService: CartService,
|
||||
private apiService: ApiService,
|
||||
private router: Router
|
||||
) {
|
||||
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) {
|
||||
clearTimeout(this.closeTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
removeItem(itemID: number): void {
|
||||
this.cartService.removeItem(itemID);
|
||||
this.swipedItemId.set(null);
|
||||
}
|
||||
|
||||
updateQuantity(itemID: number, quantity: number): void {
|
||||
this.cartService.updateQuantity(itemID, quantity);
|
||||
}
|
||||
|
||||
increaseQuantity(itemID: number, currentQuantity: number): void {
|
||||
this.updateQuantity(itemID, currentQuantity + 1);
|
||||
}
|
||||
|
||||
decreaseQuantity(itemID: number, currentQuantity: number): void {
|
||||
if (currentQuantity <= 1) {
|
||||
this.removeItem(itemID);
|
||||
} else {
|
||||
this.updateQuantity(itemID, currentQuantity - 1);
|
||||
}
|
||||
}
|
||||
|
||||
onSwipeStart(itemID: number, event: TouchEvent): void {
|
||||
const item = event.currentTarget as HTMLElement;
|
||||
const startX = event.touches[0].clientX;
|
||||
|
||||
const onMove = (e: TouchEvent) => {
|
||||
const currentX = e.touches[0].clientX;
|
||||
const diff = startX - currentX;
|
||||
|
||||
if (diff > 50) {
|
||||
this.swipedItemId.set(itemID);
|
||||
cleanup();
|
||||
} else if (diff < -10) {
|
||||
this.swipedItemId.set(null);
|
||||
cleanup();
|
||||
}
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
document.removeEventListener('touchmove', onMove as any);
|
||||
document.removeEventListener('touchend', cleanup);
|
||||
};
|
||||
|
||||
document.addEventListener('touchmove', onMove as any);
|
||||
document.addEventListener('touchend', cleanup);
|
||||
}
|
||||
|
||||
clearCart(): void {
|
||||
if (confirm('Вы уверены, что хотите очистить корзину?')) {
|
||||
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);
|
||||
}
|
||||
|
||||
checkout(): void {
|
||||
if (!this.termsAccepted) {
|
||||
alert('Пожалуйста, примите условия договора, политику возврата и гарантии для продолжения оформления заказа.');
|
||||
return;
|
||||
}
|
||||
this.openPaymentPopup();
|
||||
}
|
||||
|
||||
openPaymentPopup(): void {
|
||||
this.showPaymentPopup.set(true);
|
||||
this.paymentStatus.set('creating');
|
||||
this.userEmail.set('');
|
||||
this.userPhone.set('');
|
||||
this.emailTouched.set(false);
|
||||
this.phoneTouched.set(false);
|
||||
this.emailError.set('');
|
||||
this.phoneError.set('');
|
||||
this.emailSubmitting.set(false);
|
||||
this.paidItems = [...this.items()];
|
||||
this.createPayment();
|
||||
}
|
||||
|
||||
closePaymentPopup(): void {
|
||||
this.showPaymentPopup.set(false);
|
||||
this.stopPolling();
|
||||
if (this.closeTimeout) {
|
||||
clearTimeout(this.closeTimeout);
|
||||
this.closeTimeout = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
createPayment(): void {
|
||||
const telegramUsername = this.getTelegramUsername();
|
||||
const userId = this.getUserId();
|
||||
const orderId = this.generateOrderId();
|
||||
|
||||
const paymentData = {
|
||||
amount: this.totalPrice(),
|
||||
currency: 'RUB',
|
||||
siteuserID: userId,
|
||||
siteorderID: orderId,
|
||||
redirectUrl: '',
|
||||
telegramUsername: telegramUsername,
|
||||
items: this.items().map((item: CartItem) => ({
|
||||
itemID: item.itemID,
|
||||
price: item.discount > 0
|
||||
? item.price * (1 - item.discount / 100)
|
||||
: item.price,
|
||||
name: item.name,
|
||||
quantity: item.quantity
|
||||
}))
|
||||
};
|
||||
|
||||
this.apiService.createPayment(paymentData).subscribe({
|
||||
next: (response) => {
|
||||
this.paymentId.set(response.qrId);
|
||||
this.qrCodeUrl.set(response.qrUrl);
|
||||
this.paymentUrl.set(response.payload);
|
||||
this.paymentStatus.set('waiting');
|
||||
this.startPolling();
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error creating payment:', err);
|
||||
this.paymentStatus.set('timeout');
|
||||
this.closeTimeout = setTimeout(() => {
|
||||
this.closePaymentPopup();
|
||||
}, 4000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
startPolling(): void {
|
||||
this.pollingSubscription = interval(5000) // every 5 seconds
|
||||
.pipe(
|
||||
take(this.maxChecks), // maximum 36 checks (3 minutes)
|
||||
switchMap(() => {
|
||||
return this.apiService.checkPaymentStatus(this.paymentId());
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
// Check if payment is successful
|
||||
if (response.paymentStatus === 'SUCCESS' && response.code === 'SUCCESS') {
|
||||
this.paymentStatus.set('success');
|
||||
this.stopPolling();
|
||||
// Clear cart but don't close popup - wait for email submission
|
||||
this.cartService.clearCart();
|
||||
}
|
||||
// Continue checking for 3 minutes regardless of other statuses
|
||||
},
|
||||
complete: () => {
|
||||
this.stopPolling();
|
||||
// If all checks are done but payment not completed
|
||||
if (this.paymentStatus() === 'waiting') {
|
||||
this.paymentStatus.set('timeout');
|
||||
// Close popup after showing timeout message
|
||||
this.closeTimeout = setTimeout(() => {
|
||||
this.closePaymentPopup();
|
||||
}, 3000);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error checking payment status:', err);
|
||||
// Continue checking even on error until time runs out
|
||||
this.closeTimeout = setTimeout(() => {
|
||||
this.closePaymentPopup();
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stopPolling(): void {
|
||||
if (this.pollingSubscription) {
|
||||
this.pollingSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
copyPaymentLink(): void {
|
||||
const url = this.paymentUrl();
|
||||
if (url) {
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
this.linkCopied.set(true);
|
||||
setTimeout(() => this.linkCopied.set(false), 2000);
|
||||
}).catch(err => {
|
||||
console.error('Ошибка копирования:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private getTelegramUsername(): string {
|
||||
if (typeof window !== 'undefined' && window.Telegram?.WebApp?.initDataUnsafe?.user) {
|
||||
const user = window.Telegram.WebApp.initDataUnsafe.user;
|
||||
return user.username || 'nontelegram';
|
||||
}
|
||||
return 'nontelegram';
|
||||
}
|
||||
|
||||
private getUserId(): string {
|
||||
if (typeof window !== 'undefined' && window.Telegram?.WebApp?.initDataUnsafe?.user) {
|
||||
return window.Telegram.WebApp.initDataUnsafe.user.id.toString();
|
||||
}
|
||||
return `web_${Date.now()}`;
|
||||
}
|
||||
|
||||
private generateOrderId(): string {
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).substring(2, 8);
|
||||
return `order_${timestamp}_${random}`;
|
||||
}
|
||||
|
||||
submitEmail(): void {
|
||||
// Mark both fields as touched
|
||||
this.emailTouched.set(true);
|
||||
this.phoneTouched.set(true);
|
||||
|
||||
// Validate both fields
|
||||
this.validateEmail();
|
||||
const digitsOnly = this.userPhone().replace(/\D/g, '');
|
||||
this.validatePhone(digitsOnly);
|
||||
|
||||
// Check if there are any errors
|
||||
if (this.emailError() || this.phoneError()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const email = this.userEmail().trim();
|
||||
const phoneRaw = this.userPhone().replace(/\D/g, ''); // Remove all formatting, send only digits
|
||||
|
||||
this.emailSubmitting.set(true);
|
||||
|
||||
const emailData = {
|
||||
email: email,
|
||||
phone: phoneRaw,
|
||||
telegramUserId: this.getTelegramUserId(),
|
||||
items: this.paidItems.map((item: CartItem) => ({
|
||||
itemID: item.itemID,
|
||||
name: item.name,
|
||||
price: item.discount > 0
|
||||
? item.price * (1 - item.discount / 100)
|
||||
: item.price,
|
||||
currency: item.currency,
|
||||
quantity: item.quantity
|
||||
}))
|
||||
};
|
||||
|
||||
this.apiService.submitPurchaseEmail(emailData).subscribe({
|
||||
next: () => {
|
||||
this.emailSubmitting.set(false);
|
||||
// Show success message
|
||||
alert('Email успешно отправлен! Проверьте вашу почту.');
|
||||
// Close popup and redirect to home page
|
||||
setTimeout(() => {
|
||||
this.closePaymentPopup();
|
||||
this.router.navigate(['/']);
|
||||
}, 500);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error submitting email:', err);
|
||||
this.emailSubmitting.set(false);
|
||||
alert('Произошла ошибка при отправке email. Пожалуйста, попробуйте снова.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getTelegramUserId(): string | null {
|
||||
if (typeof window !== 'undefined' && window.Telegram?.WebApp?.initDataUnsafe?.user) {
|
||||
return window.Telegram.WebApp.initDataUnsafe.user.id.toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
onPhoneInput(event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
let value = input.value.replace(/\D/g, ''); // Remove all non-digits
|
||||
|
||||
// Auto-add +7 for Russian numbers
|
||||
if (value.length > 0 && !value.startsWith('7') && !value.startsWith('8')) {
|
||||
value = '7' + value;
|
||||
}
|
||||
|
||||
// Convert 8 to 7 for Russian format
|
||||
if (value.startsWith('8')) {
|
||||
value = '7' + value.substring(1);
|
||||
}
|
||||
|
||||
// Format: +7 (XXX) XXX-XX-XX
|
||||
let formatted = '';
|
||||
if (value.length > 0) {
|
||||
formatted = '+7';
|
||||
if (value.length > 1) {
|
||||
formatted += ' (' + value.substring(1, 4);
|
||||
}
|
||||
if (value.length >= 4) {
|
||||
formatted += ') ' + value.substring(4, 7);
|
||||
}
|
||||
if (value.length >= 7) {
|
||||
formatted += '-' + value.substring(7, 9);
|
||||
}
|
||||
if (value.length >= 9) {
|
||||
formatted += '-' + value.substring(9, 11);
|
||||
}
|
||||
}
|
||||
|
||||
this.userPhone.set(formatted);
|
||||
this.validatePhone(value);
|
||||
}
|
||||
|
||||
onPhoneBlur(): void {
|
||||
this.phoneTouched.set(true);
|
||||
const digitsOnly = this.userPhone().replace(/\D/g, '');
|
||||
this.validatePhone(digitsOnly);
|
||||
}
|
||||
|
||||
validatePhone(digitsOnly: string): void {
|
||||
if (!this.phoneTouched() && digitsOnly.length === 0) {
|
||||
this.phoneError.set('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (digitsOnly.length === 0) {
|
||||
this.phoneError.set('Номер телефона обязателен');
|
||||
} else if (digitsOnly.length < 11) {
|
||||
this.phoneError.set(`Введите еще ${11 - digitsOnly.length} цифр`);
|
||||
} else if (digitsOnly.length > 11) {
|
||||
this.phoneError.set('Слишком много цифр');
|
||||
} else {
|
||||
this.phoneError.set('');
|
||||
}
|
||||
}
|
||||
|
||||
onEmailInput(event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
this.userEmail.set(input.value);
|
||||
if (this.emailTouched()) {
|
||||
this.validateEmail();
|
||||
}
|
||||
}
|
||||
|
||||
onEmailBlur(): void {
|
||||
this.emailTouched.set(true);
|
||||
this.validateEmail();
|
||||
}
|
||||
|
||||
validateEmail(): void {
|
||||
const email = this.userEmail().trim();
|
||||
|
||||
if (!this.emailTouched() && email.length === 0) {
|
||||
this.emailError.set('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (email.length === 0) {
|
||||
this.emailError.set('Email обязателен');
|
||||
} else if (email.length < 5) {
|
||||
this.emailError.set('Email слишком короткий (минимум 5 символов)');
|
||||
} else if (email.length > 100) {
|
||||
this.emailError.set('Email слишком длинный (максимум 100 символов)');
|
||||
} else if (!email.includes('@')) {
|
||||
this.emailError.set('Email должен содержать @');
|
||||
} else if (!email.includes('.')) {
|
||||
this.emailError.set('Email должен содержать домен (.com, .ru и т.д.)');
|
||||
} else {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
this.emailError.set('Некорректный формат email');
|
||||
} else {
|
||||
this.emailError.set('');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user