Merge branch 'back-office-integration'

# Conflicts:
#	src/app/pages/cart/cart.component.ts
#	src/app/pages/category/category.component.html
#	src/app/pages/category/category.component.ts
#	src/app/pages/item-detail/item-detail.component.html
#	src/app/pages/item-detail/item-detail.component.ts
#	src/app/pages/legal/company-details/en/company-details-en.component.html
#	src/app/pages/legal/company-details/hy/company-details-hy.component.html
#	src/app/pages/legal/company-details/ru/company-details-ru.component.html
#	src/app/pages/legal/public-offer/en/public-offer-en.component.html
#	src/app/pages/legal/public-offer/ru/public-offer-ru.component.html
#	src/app/pages/search/search.component.ts
#	src/app/services/api.service.ts
This commit is contained in:
sdarbinyan
2026-03-24 00:18:13 +04:00
76 changed files with 6828 additions and 2331 deletions

View File

@@ -27,6 +27,7 @@
</nav>
<div class="novo-right">
<app-region-selector />
<app-language-selector />
<a [routerLink]="'/cart' | langRoute" routerLinkActive="novo-cart-active" class="novo-cart" (click)="closeMenu()" [attr.aria-label]="'header.cart' | translate">
@@ -106,6 +107,11 @@
}
</a>
<!-- Region Selector (desktop only) -->
<div class="dexar-region-selector dexar-lang-desktop">
<app-region-selector />
</div>
<!-- Language Selector (desktop only) -->
<div class="dexar-lang-selector dexar-lang-desktop">
<app-language-selector />
@@ -171,6 +177,11 @@
</svg>
</a>
<!-- Region Selector in mobile menu -->
<div class="dexar-mobile-lang">
<app-region-selector />
</div>
<!-- Language Selector in mobile menu -->
<div class="dexar-mobile-lang">
<app-language-selector />

View File

@@ -4,12 +4,13 @@ import { CartService, LanguageService } from '../../services';
import { environment } from '../../../environments/environment';
import { LogoComponent } from '../logo/logo.component';
import { LanguageSelectorComponent } from '../language-selector/language-selector.component';
import { RegionSelectorComponent } from '../region-selector/region-selector.component';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe';
@Component({
selector: 'app-header',
imports: [RouterLink, RouterLinkActive, LogoComponent, LanguageSelectorComponent, LangRoutePipe, TranslatePipe],
imports: [RouterLink, RouterLinkActive, LogoComponent, LanguageSelectorComponent, RegionSelectorComponent, LangRoutePipe, TranslatePipe],
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush

View File

@@ -18,14 +18,21 @@
<div class="item-card">
<a [routerLink]="['/item', product.itemID] | langRoute" class="item-link">
<div class="item-image">
<img [src]="getItemImage(product)" [alt]="product.name" loading="lazy" />
<img [src]="getItemImage(product)" [alt]="itemName(product)" loading="lazy" />
@if (product.discount > 0) {
<span class="discount-badge">-{{ product.discount }}%</span>
}
@if (product.badges && product.badges.length > 0) {
<div class="item-badges-overlay">
@for (badge of product.badges; track badge) {
<span class="item-badge" [class]="getBadgeClass(badge)">{{ badge }}</span>
}
</div>
}
</div>
<div class="item-details">
<h3 class="item-name">{{ product.name }}</h3>
<h3 class="item-name">{{ itemName(product) }}</h3>
@if (product.rating) {
<div class="item-rating">

View File

@@ -1,4 +1,4 @@
import { Component, OnInit, signal, ChangeDetectionStrategy } from '@angular/core';
import { Component, OnInit, signal, ChangeDetectionStrategy, inject } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import { RouterLink } from '@angular/router';
import { CarouselModule } from 'primeng/carousel';
@@ -7,7 +7,8 @@ import { TagModule } from 'primeng/tag';
import { ApiService, CartService } from '../../services';
import { Item } from '../../models';
import { environment } from '../../../environments/environment';
import { getDiscountedPrice, getMainImage } from '../../utils/item.utils';
import { getDiscountedPrice, getMainImage, getBadgeClass, getTranslatedField } from '../../utils/item.utils';
import { LanguageService } from '../../services/language.service';
import { LangRoutePipe } from '../../pipes/lang-route.pipe';
import { TranslatePipe } from '../../i18n/translate.pipe';
@@ -98,6 +99,10 @@ export class ItemsCarouselComponent implements OnInit {
readonly getItemImage = getMainImage;
readonly getDiscountedPrice = getDiscountedPrice;
readonly getBadgeClass = getBadgeClass;
private langService = inject(LanguageService);
itemName(product: Item): string { return getTranslatedField(product, 'name', this.langService.currentLanguage()); }
addToCart(event: Event, item: Item): void {
event.preventDefault();

View File

@@ -24,4 +24,25 @@
</button>
}
</div>
<button class="currency-button" (click)="toggleCurrency()">
<span class="currency-symbol">{{ languageService.getCurrentCurrency()?.symbol }}</span>
<span class="currency-code">{{ languageService.currentCurrency() }}</span>
<svg class="dropdown-arrow" [class.rotated]="currencyOpen" width="12" height="12" viewBox="0 0 12 12" fill="none">
<path d="M2.5 4.5L6 8L9.5 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div class="currency-dropdown" [class.open]="currencyOpen">
@for (cur of languageService.currencies; track cur.code) {
<button
class="currency-option"
[class.active]="languageService.currentCurrency() === cur.code"
(click)="selectCurrency(cur)">
<span class="cur-symbol">{{ cur.symbol }}</span>
<span class="cur-name">{{ cur.name }}</span>
<span class="cur-code">{{ cur.code }}</span>
</button>
}
</div>
</div>

View File

@@ -301,3 +301,162 @@
}
}
}
// ── Currency selector ──
.language-selector {
display: inline-flex;
align-items: center;
gap: 6px;
}
.currency-button {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 10px;
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
color: #ffffff;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
font-weight: 500;
&:hover {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.3);
}
.currency-symbol {
font-size: 15px;
font-weight: 700;
}
.currency-code {
font-size: 13px;
letter-spacing: 0.5px;
}
.dropdown-arrow {
transition: transform 0.3s ease;
opacity: 0.7;
&.rotated { transform: rotate(180deg); }
}
}
.currency-dropdown {
position: absolute;
top: calc(100% + 8px);
right: 0;
min-width: 170px;
background: var(--card-bg, #1a1a1a);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.3s ease;
z-index: 1000;
overflow: hidden;
&.open {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
}
.currency-option {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 11px 16px;
background: transparent;
border: none;
color: #ffffff;
cursor: pointer;
transition: background 0.2s ease;
font-size: 14px;
&:hover {
background: rgba(255, 255, 255, 0.05);
}
&.active {
background: rgba(255, 255, 255, 0.1);
font-weight: 600;
}
.cur-symbol {
font-size: 16px;
font-weight: 700;
width: 20px;
text-align: center;
}
.cur-name {
flex: 1;
}
.cur-code {
font-size: 12px;
opacity: 0.6;
}
}
// Light / Novo / Dexar theme adjustments for currency
:host-context(.novo-header),
:host-context(.header) {
.currency-button {
border-color: rgba(0, 0, 0, 0.2);
color: #333333;
&:hover {
background: rgba(0, 0, 0, 0.05);
border-color: rgba(0, 0, 0, 0.3);
}
}
}
:host-context(.light-theme) {
.currency-dropdown {
background: #ffffff;
border-color: rgba(0, 0, 0, 0.1);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.currency-option {
color: #333333;
&:hover { background: rgba(0, 0, 0, 0.05); }
&.active { background: rgba(0, 0, 0, 0.1); }
}
}
:host-context(.dexar-header),
:host-context(.dexar-mobile-menu) {
.currency-button {
padding: 4px 8px;
gap: 3px;
background: rgba(255, 255, 255, 0.3);
border: 1px solid #677b78;
border-radius: 8px;
color: #1e3c38;
&:hover {
background: rgba(255, 255, 255, 0.5);
}
.currency-symbol { font-size: 14px; color: #1e3c38; }
.currency-code { font-size: 13px; color: #1e3c38; }
.dropdown-arrow path { stroke: #1e3c38; }
}
.currency-dropdown {
background: #ffffff;
border-color: #d3dad9;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.currency-option {
color: #1e3c38;
&:hover { background: rgba(161, 180, 181, 0.2); }
&.active { background: rgba(73, 118, 113, 0.1); }
}
}

View File

@@ -1,5 +1,5 @@
import { Component, HostListener, ElementRef, ChangeDetectionStrategy } from '@angular/core';
import { LanguageService, Language } from '../../services/language.service';
import { LanguageService, Language, Currency } from '../../services/language.service';
@Component({
selector: 'app-language-selector',
@@ -10,6 +10,7 @@ import { LanguageService, Language } from '../../services/language.service';
})
export class LanguageSelectorComponent {
dropdownOpen = false;
currencyOpen = false;
constructor(
public languageService: LanguageService,
@@ -18,6 +19,12 @@ export class LanguageSelectorComponent {
toggleDropdown(): void {
this.dropdownOpen = !this.dropdownOpen;
this.currencyOpen = false;
}
toggleCurrency(): void {
this.currencyOpen = !this.currencyOpen;
this.dropdownOpen = false;
}
selectLanguage(lang: Language): void {
@@ -27,8 +34,14 @@ export class LanguageSelectorComponent {
}
}
selectCurrency(currency: Currency): void {
this.languageService.setCurrency(currency.code);
this.currencyOpen = false;
}
closeDropdown(): void {
this.dropdownOpen = false;
this.currencyOpen = false;
}
onKeyDown(event: KeyboardEvent): void {
@@ -44,6 +57,7 @@ export class LanguageSelectorComponent {
onClickOutside(event: Event): void {
if (!this.elementRef.nativeElement.contains(event.target)) {
this.dropdownOpen = false;
this.currencyOpen = false;
}
}
}

View File

@@ -0,0 +1,54 @@
<div class="region-selector">
<button class="region-trigger" (click)="toggleDropdown()" [class.active]="dropdownOpen()">
<svg class="pin-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
<span class="region-name">
@if (detecting()) {
<span class="detecting">...</span>
} @else if (region()) {
{{ region()!.city }}
} @else {
{{ 'location.allRegions' | translate }}
}
</span>
<svg class="chevron" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"
[class.rotated]="dropdownOpen()">
<path d="M6 9l6 6 6-6"></path>
</svg>
</button>
@if (dropdownOpen()) {
<div class="region-dropdown">
<div class="dropdown-header">
<span>{{ 'location.chooseRegion' | translate }}</span>
@if (!detecting()) {
<button class="detect-btn" (click)="detectLocation()" title="{{ 'location.detectAuto' | translate }}">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>
<path d="M12 2v4M12 18v4M2 12h4M18 12h4"></path>
</svg>
</button>
}
</div>
<div class="region-list">
<button class="region-option" [class.selected]="!region()" (click)="selectGlobal()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
</svg>
<span>{{ 'location.allRegions' | translate }}</span>
</button>
@for (r of regions(); track r.id) {
<button class="region-option" [class.selected]="region()?.id === r.id" (click)="selectRegion(r)">
<span class="region-city">{{ r.city }}</span>
<span class="region-country">{{ r.country }}</span>
</button>
}
</div>
</div>
}
</div>

View File

@@ -0,0 +1,180 @@
.region-selector {
position: relative;
}
.region-trigger {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 8px;
background: transparent;
cursor: pointer;
font-size: 13px;
color: var(--text-primary, #333);
transition: all 0.2s ease;
white-space: nowrap;
&:hover, &.active {
border-color: var(--accent-color, #497671);
background: var(--bg-hover, rgba(73, 118, 113, 0.05));
}
.pin-icon {
flex-shrink: 0;
color: var(--accent-color, #497671);
}
.region-name {
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
.detecting {
animation: pulse 1s ease infinite;
}
}
.chevron {
flex-shrink: 0;
transition: transform 0.2s ease;
&.rotated {
transform: rotate(180deg);
}
}
}
.region-dropdown {
position: absolute;
top: calc(100% + 4px);
left: 0;
min-width: 220px;
background: var(--bg-card, #fff);
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
z-index: 1000;
overflow: hidden;
animation: slideDown 0.15s ease;
}
.dropdown-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
border-bottom: 1px solid var(--border-color, #e0e0e0);
font-size: 12px;
font-weight: 600;
color: var(--text-secondary, #666);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.detect-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
border-radius: 6px;
background: var(--bg-hover, rgba(73, 118, 113, 0.08));
color: var(--accent-color, #497671);
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: var(--accent-color, #497671);
color: #fff;
}
}
.region-list {
max-height: 280px;
overflow-y: auto;
padding: 4px;
}
.region-option {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 10px 12px;
border: none;
border-radius: 8px;
background: transparent;
cursor: pointer;
font-size: 14px;
color: var(--text-primary, #333);
text-align: left;
transition: background 0.15s ease;
&:hover {
background: var(--bg-hover, rgba(73, 118, 113, 0.06));
}
&.selected {
background: var(--accent-color, #497671);
color: #fff;
.region-country {
color: rgba(255, 255, 255, 0.7);
}
}
.region-city {
flex: 1;
}
.region-country {
font-size: 12px;
color: var(--text-secondary, #999);
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
// Mobile adjustments
@media (max-width: 768px) {
.region-trigger {
padding: 5px 8px;
font-size: 12px;
.region-name {
max-width: 80px;
}
}
.region-dropdown {
position: fixed;
top: auto;
bottom: 0;
left: 0;
right: 0;
min-width: 100%;
border-radius: 16px 16px 0 0;
max-height: 60vh;
.region-list {
max-height: 50vh;
}
}
}

View File

@@ -0,0 +1,47 @@
import { Component, ChangeDetectionStrategy, inject, signal, HostListener } from '@angular/core';
import { LocationService } from '../../services/location.service';
import { Region } from '../../models/location.model';
import { TranslatePipe } from '../../i18n/translate.pipe';
@Component({
selector: 'app-region-selector',
imports: [TranslatePipe],
templateUrl: './region-selector.component.html',
styleUrls: ['./region-selector.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RegionSelectorComponent {
private locationService = inject(LocationService);
region = this.locationService.region;
regions = this.locationService.regions;
detecting = this.locationService.detecting;
dropdownOpen = signal(false);
toggleDropdown(): void {
this.dropdownOpen.update(v => !v);
}
selectRegion(region: Region): void {
this.locationService.setRegion(region);
this.dropdownOpen.set(false);
}
selectGlobal(): void {
this.locationService.clearRegion();
this.dropdownOpen.set(false);
}
detectLocation(): void {
this.locationService.detectLocation();
}
@HostListener('document:click', ['$event'])
onDocumentClick(event: MouseEvent): void {
const target = event.target as HTMLElement;
if (!target.closest('app-region-selector')) {
this.dropdownOpen.set(false);
}
}
}

View File

@@ -0,0 +1,47 @@
@if (showDialog()) {
<div class="login-overlay" (click)="close()">
<div class="login-dialog" (click)="$event.stopPropagation()">
<button class="close-btn" (click)="close()">
<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 class="login-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
</svg>
</div>
<h2>{{ 'auth.loginRequired' | translate }}</h2>
<p class="login-desc">{{ 'auth.loginDescription' | translate }}</p>
@if (status() === 'checking') {
<div class="login-status checking">
<div class="spinner"></div>
<span>{{ 'auth.checking' | translate }}</span>
</div>
} @else {
<button class="telegram-btn" (click)="openTelegramLogin()">
<svg class="tg-icon" width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
</svg>
{{ 'auth.loginWithTelegram' | translate }}
</button>
<div class="qr-section">
<p class="qr-hint">{{ 'auth.orScanQr' | translate }}</p>
<div class="qr-container">
<img [src]="'https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=' + loginUrl()"
alt="QR Code"
width="180"
height="180"
loading="lazy" />
</div>
</div>
<p class="login-note">{{ 'auth.loginNote' | translate }}</p>
}
</div>
</div>
}

View File

@@ -0,0 +1,184 @@
.login-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
animation: fadeIn 0.2s ease;
padding: 16px;
}
.login-dialog {
position: relative;
background: var(--bg-card, #fff);
border-radius: 20px;
padding: 32px 28px;
max-width: 400px;
width: 100%;
text-align: center;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
animation: scaleIn 0.25s ease;
}
.close-btn {
position: absolute;
top: 12px;
right: 12px;
width: 32px;
height: 32px;
border: none;
border-radius: 50%;
background: var(--bg-hover, #f0f0f0);
color: var(--text-secondary, #666);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
&:hover {
background: #e0e0e0;
color: #333;
}
}
.login-icon {
margin: 0 auto 16px;
width: 72px;
height: 72px;
border-radius: 50%;
background: var(--accent-light, rgba(73, 118, 113, 0.1));
color: var(--accent-color, #497671);
display: flex;
align-items: center;
justify-content: center;
}
h2 {
margin: 0 0 8px;
font-size: 20px;
font-weight: 700;
color: var(--text-primary, #1a1a1a);
}
.login-desc {
margin: 0 0 24px;
font-size: 14px;
color: var(--text-secondary, #666);
line-height: 1.5;
}
.telegram-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
width: 100%;
padding: 14px 24px;
border: none;
border-radius: 12px;
background: #2AABEE;
color: #fff;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: #229ED9;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(42, 171, 238, 0.3);
}
&:active {
transform: translateY(0);
}
.tg-icon {
flex-shrink: 0;
}
}
.qr-section {
margin-top: 20px;
.qr-hint {
margin: 0 0 12px;
font-size: 13px;
color: var(--text-secondary, #999);
}
.qr-container {
display: inline-flex;
padding: 12px;
background: #fff;
border-radius: 12px;
border: 1px solid #e8e8e8;
img {
display: block;
border-radius: 4px;
}
}
}
.login-note {
margin: 16px 0 0;
font-size: 12px;
color: var(--text-secondary, #999);
line-height: 1.4;
}
.login-status {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 16px;
color: var(--text-secondary, #666);
font-size: 14px;
.spinner {
width: 20px;
height: 20px;
border: 2px solid #e0e0e0;
border-top-color: var(--accent-color, #497671);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@media (max-width: 480px) {
.login-dialog {
padding: 24px 20px;
border-radius: 16px;
}
.qr-section .qr-container img {
width: 140px;
height: 140px;
}
}

View File

@@ -0,0 +1,66 @@
import { Component, ChangeDetectionStrategy, inject, signal, OnInit, OnDestroy } from '@angular/core';
import { AuthService } from '../../services/auth.service';
import { TranslatePipe } from '../../i18n/translate.pipe';
@Component({
selector: 'app-telegram-login',
imports: [TranslatePipe],
templateUrl: './telegram-login.component.html',
styleUrls: ['./telegram-login.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TelegramLoginComponent implements OnInit, OnDestroy {
private authService = inject(AuthService);
showDialog = this.authService.showLoginDialog;
status = this.authService.status;
loginUrl = signal('');
private pollTimer?: ReturnType<typeof setInterval>;
ngOnInit(): void {
this.loginUrl.set(this.authService.getTelegramLoginUrl());
}
ngOnDestroy(): void {
this.stopPolling();
}
close(): void {
this.authService.hideLogin();
this.stopPolling();
}
/** Open Telegram login link and start polling for session */
openTelegramLogin(): void {
window.open(this.loginUrl(), '_blank');
this.startPolling();
}
/** Start polling the backend to detect when user completes Telegram auth */
private startPolling(): void {
this.stopPolling();
// Check every 3 seconds for up to 5 minutes
let checks = 0;
this.pollTimer = setInterval(() => {
checks++;
if (checks > 100) { // 100 * 3s = 5 min
this.stopPolling();
return;
}
this.authService.checkSession();
// If authenticated, stop polling and close dialog
if (this.authService.isAuthenticated()) {
this.stopPolling();
this.authService.hideLogin();
}
}, 3000);
}
private stopPolling(): void {
if (this.pollTimer) {
clearInterval(this.pollTimer);
this.pollTimer = undefined;
}
}
}