From 56f4c56b9e5a8ea02f4abf55323a14700dcc3dcd Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Tue, 24 Mar 2026 00:09:11 +0400 Subject: [PATCH] integration new apis --- angular.json | 3 +- .../items-carousel.component.html | 4 +- .../items-carousel.component.ts | 8 +- .../language-selector.component.html | 21 + .../language-selector.component.scss | 159 ++++++ .../language-selector.component.ts | 16 +- src/app/i18n/en.ts | 7 + src/app/i18n/hy.ts | 352 ++++++------- src/app/i18n/ru.ts | 7 + src/app/i18n/translations.ts | 7 + .../interceptors/api-headers.interceptor.ts | 39 +- src/app/interceptors/mock-data.interceptor.ts | 30 ++ src/app/models/item.model.ts | 27 +- src/app/pages/cart/cart.component.html | 63 ++- src/app/pages/cart/cart.component.scss | 111 ++++ src/app/pages/cart/cart.component.ts | 19 +- .../pages/category/category.component.html | 4 +- src/app/pages/category/category.component.ts | 10 +- .../category/subcategories.component.html | 72 ++- .../category/subcategories.component.scss | 158 ++++++ .../pages/category/subcategories.component.ts | 77 ++- .../pages/info/faq/en/faq-en.component.html | 482 +++++++++--------- .../pages/info/faq/hy/faq-hy.component.html | 482 +++++++++--------- .../pages/info/faq/ru/faq-ru.component.html | 482 +++++++++--------- .../item-detail/item-detail.component.html | 56 ++ .../item-detail/item-detail.component.scss | 58 +++ .../en/company-details-en.component.html | 198 +++---- .../hy/company-details-hy.component.html | 198 +++---- .../ru/company-details-ru.component.html | 198 +++---- .../en/payment-terms-en.component.html | 228 +++++---- .../hy/payment-terms-hy.component.html | 228 +++++---- .../ru/payment-terms-ru.component.html | 228 +++++---- .../en/privacy-policy-en.component.html | 4 + .../hy/privacy-policy-hy.component.html | 4 + .../ru/privacy-policy-ru.component.html | 4 + .../en/public-offer-en.component.html | 4 + .../hy/public-offer-hy.component.html | 6 +- .../ru/public-offer-ru.component.html | 4 + .../en/return-policy-en.component.html | 4 + .../hy/return-policy-hy.component.html | 6 +- .../ru/return-policy-ru.component.html | 4 + src/app/pages/search/search.component.html | 8 +- src/app/pages/search/search.component.ts | 9 +- src/app/services/api.service.ts | 39 +- src/app/services/language.service.ts | 32 ++ src/app/utils/item.utils.ts | 16 +- src/environments/environment.ts | 4 +- 47 files changed, 2603 insertions(+), 1577 deletions(-) diff --git a/angular.json b/angular.json index edace0f..ff112fd 100644 --- a/angular.json +++ b/angular.json @@ -154,7 +154,8 @@ }, "serve": { "options": { - "allowedHosts": ["novo.market", "dexarmarket.ru", "localhost"] + "allowedHosts": ["novo.market", "dexarmarket.ru", "localhost"], + "proxyConfig": "proxy.conf.json" }, "builder": "@angular/build:dev-server", "configurations": { diff --git a/src/app/components/items-carousel/items-carousel.component.html b/src/app/components/items-carousel/items-carousel.component.html index 49b5d77..3f03f84 100644 --- a/src/app/components/items-carousel/items-carousel.component.html +++ b/src/app/components/items-carousel/items-carousel.component.html @@ -18,7 +18,7 @@
- + @if (product.discount > 0) { -{{ product.discount }}% } @@ -32,7 +32,7 @@
-

{{ product.name }}

+

{{ itemName(product) }}

@if (product.rating) {
diff --git a/src/app/components/items-carousel/items-carousel.component.ts b/src/app/components/items-carousel/items-carousel.component.ts index 44275d9..dda963c 100644 --- a/src/app/components/items-carousel/items-carousel.component.ts +++ b/src/app/components/items-carousel/items-carousel.component.ts @@ -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, getBadgeClass } 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'; @@ -100,6 +101,9 @@ export class ItemsCarouselComponent implements OnInit { 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(); event.stopPropagation(); diff --git a/src/app/components/language-selector/language-selector.component.html b/src/app/components/language-selector/language-selector.component.html index bcfb994..4c642fe 100644 --- a/src/app/components/language-selector/language-selector.component.html +++ b/src/app/components/language-selector/language-selector.component.html @@ -22,4 +22,25 @@ }
+ + + +
+ @for (cur of languageService.currencies; track cur.code) { + + } +
diff --git a/src/app/components/language-selector/language-selector.component.scss b/src/app/components/language-selector/language-selector.component.scss index d1a2fe0..b720b27 100644 --- a/src/app/components/language-selector/language-selector.component.scss +++ b/src/app/components/language-selector/language-selector.component.scss @@ -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); } + } +} diff --git a/src/app/components/language-selector/language-selector.component.ts b/src/app/components/language-selector/language-selector.component.ts index e6684e8..3f7f793 100644 --- a/src/app/components/language-selector/language-selector.component.ts +++ b/src/app/components/language-selector/language-selector.component.ts @@ -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,14 +34,21 @@ export class LanguageSelectorComponent { } } + selectCurrency(currency: Currency): void { + this.languageService.setCurrency(currency.code); + this.currencyOpen = false; + } + closeDropdown(): void { this.dropdownOpen = false; + this.currencyOpen = false; } @HostListener('document:click', ['$event']) onClickOutside(event: Event): void { if (!this.elementRef.nativeElement.contains(event.target)) { this.dropdownOpen = false; + this.currencyOpen = false; } } } diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index cc90315..512ddc6 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -102,6 +102,10 @@ export const en: Translations = { emailNeedsAt: 'Email must contain @', emailNeedsDomain: 'Email must contain a domain (.com, .ru, etc.)', emailInvalid: 'Invalid email format', + loginRequired: 'Log in to checkout', + loginRequiredDesc: 'Please log in via Telegram to place your order', + loginWithTelegram: 'Log in with Telegram', + orScanQr: 'Or scan the QR code', }, search: { title: 'Product search', @@ -134,6 +138,7 @@ export const en: Translations = { emptyTitle: 'Oops! No subcategories yet', emptyDesc: 'There are no subcategories in this section yet, but they will appear soon', goHome: 'Go home', + itemsInCategory: 'Items in this category', }, itemDetail: { loading: 'Loading...', @@ -170,6 +175,8 @@ export const en: Translations = { yesterday: 'Yesterday', daysAgo: 'd. ago', weeksAgo: 'w. ago', + colour: 'Colour', + size: 'Size', }, app: { connecting: 'Connecting to server...', diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index a1ff5bc..9f52e59 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -2,202 +2,208 @@ import { Translations } from './translations'; export const hy: Translations = { header: { - home: '╘│╒м╒н╒б╒╛╒╕╓А', - search: '╒И╓А╒╕╒╢╒╕╓В╒┤', - about: '╒Д╒е╓А ╒┤╒б╒╜╒л╒╢', - contacts: '╘┐╒б╒║', - searchPlaceholder: '╒И╓А╒╕╒╢╒е╒м...', - catalog: '╘┐╒б╒┐╒б╒м╒╕╒г', + home: 'Գլխավոր', + search: 'Որոնում', + about: 'Մեր մասին', + contacts: 'Կապ', + searchPlaceholder: 'Փնտրել...', + catalog: 'Կատալոգ', }, footer: { - description: '╘║╒б╒┤╒б╒╢╒б╒п╒б╒п╒л╓Б ╒┤╒б╓А╓Д╒е╒й╓Г╒м╒е╒╡╒╜ ╒░╒б╓А╒┤╒б╓А ╒г╒╢╒╕╓В╒┤╒╢╒е╓А╒л ╒░╒б╒┤╒б╓А', - company: '╘╕╒╢╒п╒е╓А╒╕╓В╒й╒╡╒╕╓В╒╢', - aboutUs: '╒Д╒е╓А ╒┤╒б╒╜╒л╒╢', - contacts: '╘┐╒б╒║', - requisites: '╒О╒б╒╛╒е╓А╒б╒║╒б╒╡╒┤╒б╒╢╒╢╒е╓А', - support: '╘▒╒╗╒б╒п╓Б╒╕╓В╒й╒╡╒╕╓В╒╢', - faq: '╒А╒П╒А', - delivery: '╘▒╒╝╒б╓Д╒╕╓В╒┤', - guarantee: '╘╡╓А╒б╒╖╒н╒л╓Д', - legal: '╘╗╓А╒б╒╛╒б╒п╒б╒╢ ╒┐╒е╒▓╒е╒п╒б╒┐╒╛╒╕╓В╒й╒╡╒╕╓В╒╢', - offer: '╒Х╓Ж╒е╓А╒┐╒б', - privacy: '╘│╒б╒▓╒┐╒╢╒л╒╕╓В╒й╒╡╒╕╓В╒╢', - returns: '╒О╒е╓А╒б╒д╒б╓А╒▒', - info: '╒П╒е╒▓╒е╒п╒б╒┐╒╛╒╕╓В╒й╒╡╒╕╓В╒╢', - aboutCompany: '╘╕╒╢╒п╒е╓А╒╕╓В╒й╒╡╒б╒╢ ╒┤╒б╒╜╒л╒╢', - documents: '╒У╒б╒╜╒┐╒б╒й╒▓╒й╒е╓А', - paymentRules: '╒О╒│╒б╓А╒┤╒б╒╢ ╒п╒б╒╢╒╕╒╢╒╢╒е╓А', - returnPolicy: '╒О╒е╓А╒б╒д╒б╓А╒▒╒л ╓Д╒б╒▓╒б╓Д╒б╒п╒б╒╢╒╕╓В╒й╒╡╒╕╓В╒╢', - publicOffer: '╒А╒б╒╢╓А╒б╒╡╒л╒╢ ╓Е╓Ж╒е╓А╒┐╒б', - help: '╒Х╒г╒╢╒╕╓В╒й╒╡╒╕╓В╒╢', - payment: '╒О╒│╒б╓А╒╕╓В╒┤', - allRightsReserved: '╘▓╒╕╒м╒╕╓А ╒л╓А╒б╒╛╒╕╓В╒╢╓Д╒╢╒е╓А╒и ╒║╒б╒╖╒┐╒║╒б╒╢╒╛╒б╒о ╒е╒╢╓Й', + description: 'Ժամանակակից մարքեթփլեյս հարմար գնումների համար', + company: 'Ընկերություն', + aboutUs: 'Մեր մասին', + contacts: 'Կապ', + requisites: 'Վճարային տվյալներ', + support: 'Աջակցություն', + faq: 'ՀՏՀ', + delivery: 'Առաքում', + guarantee: 'Երաշխիք', + legal: 'Իրավական տեղեկատվություն', + offer: 'Օֆերտա', + privacy: 'Գաղտնիություն', + returns: 'Վերադարձ', + info: 'Տեղեկատվություն', + aboutCompany: 'Ընկերության մասին', + documents: 'Փաստաթղթեր', + paymentRules: 'Վճարման կանոններ', + returnPolicy: 'Վերադարձի քաղաքականություն', + publicOffer: 'Հանրային օֆերտա', + help: 'Օգնություն', + payment: 'Վճարում', + allRightsReserved: 'Բոլոր իրավունքները պաշտպանված են։', }, home: { - welcomeTo: '╘▓╒б╓А╒л ╒г╒б╒м╒╕╓В╒╜╒┐ {{brand}}', - subtitle: '╘│╒┐╒е╓Д ╒б╒┤╒е╒╢ ╒л╒╢╒╣ ╒┤╒е╒п ╒╛╒б╒╡╓А╒╕╓В╒┤', - startSearch: '╒Н╒п╒╜╒е╒м ╒╕╓А╒╕╒╢╒╕╓В╒┤╒и', - loading: '╘┐╒б╒┐╒е╒г╒╕╓А╒л╒б╒╢╒е╓А╒и ╒в╒е╒╝╒╢╒╛╒╕╓В╒┤ ╒е╒╢...', - errorTitle: '╘╗╒╢╒╣-╒╕╓А ╒в╒б╒╢ ╒╜╒н╒б╒м ╒з ╒г╒╢╒б╓Б╒е╒м', - retry: '╒У╒╕╓А╒▒╒е╒м ╒п╓А╒п╒л╒╢', - categoriesTitle: '╘▒╒║╓А╒б╒╢╓Д╒╢╒е╓А╒л ╒п╒б╒┐╒е╒г╒╕╓А╒л╒б╒╢╒е╓А', - categoriesSubtitle: '╘╕╒╢╒┐╓А╒е╓Д ╒░╒е╒┐╒б╓Д╓А╓Д╓А╒╕╒▓ ╒п╒б╒┐╒е╒г╒╕╓А╒л╒б╒╢', - categoriesEmpty: '╘┐╒б╒┐╒е╒г╒╕╓А╒л╒б╒╢╒е╓А╒и ╒╖╒╕╓В╒┐╒╕╒╛ ╒п╒░╒б╒╡╒┐╒╢╒╛╒е╒╢', - categoriesEmptyDesc: '╒Д╒е╒╢╓Д ╒б╒╖╒н╒б╒┐╒╕╓В╒┤ ╒е╒╢╓Д ╒п╒б╒┐╒б╒м╒╕╒г╒л ╒░╒б╒┤╒б╒м╓А╒┤╒б╒╢ ╒╛╓А╒б', - dexarHeroTitle: '╘▒╒╡╒╜╒┐╒е╒▓ ╒д╒╕╓В ╒п╒г╒┐╒╢╒е╒╜ ╒б╒┤╒е╒╢ ╒л╒╢╒╣', - dexarHeroSubtitle: '╒А╒б╒ж╒б╓А╒б╒╛╒╕╓А ╒б╒║╓А╒б╒╢╓Д╒╢╒е╓А ╒┤╒е╒п ╒╛╒б╒╡╓А╒╕╓В╒┤', - dexarHeroTagline: '╒║╒б╓А╒ж ╓З ╒░╒б╓А╒┤╒б╓А', - goToCatalog: '╘▒╒╢╓Б╒╢╒е╒м ╒п╒б╒┐╒б╒м╒╕╒г', - findProduct: '╘│╒┐╒╢╒е╒м ╒б╒║╓А╒б╒╢╓Д', - loadingDexar: '╘┐╒б╒┐╒е╒г╒╕╓А╒л╒б╒╢╒е╓А╒и ╒в╒е╒╝╒╢╒╛╒╕╓В╒┤ ╒е╒╢...', - catalogTitle: '╘▒╒║╓А╒б╒╢╓Д╒╢╒е╓А╒л ╒п╒б╒┐╒б╒м╒╕╒г', - emptyCategoriesDexar: '╘┐╒б╒┐╒е╒г╒╕╓А╒л╒б╒╢╒е╓А╒и ╒д╒е╒╝ ╒╣╒п╒б╒╢', - categoriesSoonDexar: '╒З╒╕╓В╒┐╒╕╒╛ ╒б╒╡╒╜╒┐╒е╒▓ ╒п╒░╒б╒╡╒┐╒╢╒╛╒е╒╢ ╒б╒║╓А╒б╒╢╓Д╒╢╒е╓А╒л ╒п╒б╒┐╒е╒г╒╕╓А╒л╒б╒╢╒е╓А', - itemsCount: '{{count}} ╒б╒║╓А╒б╒╢╓Д', + welcomeTo: 'Բարի գալուստ {{brand}}', + subtitle: 'Գտեք այն ամենը, ինչ պետք է՝ մեկ վայրում', + startSearch: 'Սկսել որոնումը', + loading: 'Բեռնում ենք կատեգորիաները...', + errorTitle: 'Ինչ-որ բան սխալ գնաց', + retry: 'Փորձել կրկին', + categoriesTitle: 'Ապրանքների կատեգորիաներ', + categoriesSubtitle: 'Ընտրեք ձեզ հետաքրքիր կատեգորիան', + categoriesEmpty: 'Կատեգորիաները շուտով կհայտնվեն', + categoriesEmptyDesc: 'Մենք աշխատում ենք կատալոգի լրացման վրա', + dexarHeroTitle: 'Այստեղ կգտնես ամեն ինչ', + dexarHeroSubtitle: 'Հազարավոր ապրանքներ մեկ վայրում', + dexarHeroTagline: 'պարզ և հարմար', + goToCatalog: 'Գնալ կատալոգ', + findProduct: 'Գտնել ապրանք', + loadingDexar: 'Կատեգորիաների բեռնում...', + catalogTitle: 'Ապրանքների կատալոգ', + emptyCategoriesDexar: 'Կատեգորիաները դեռ չկան', + categoriesSoonDexar: 'Շուտով այստեղ կհայտնվեն կատեգորիաներ', + itemsCount: '{{count}} ապրանք', }, cart: { - title: '╘╢╒б╒┤╒в╒╡╒╕╓В╒▓', - clear: '╒Д╒б╓Д╓А╒е╒м', - empty: '╘╢╒б╒┤╒в╒╡╒╕╓В╒▓╒и ╒д╒б╒┐╒б╓А╒п ╒з', - emptyDesc: '╘▒╒╛╒е╒м╒б╓Б╓А╒е╓Д ╒б╒║╓А╒б╒╢╓Д╒╢╒е╓А ╒г╒╢╒╕╓В╒┤╒╢╒е╓А╒и ╒╜╒п╒╜╒е╒м╒╕╓В ╒░╒б╒┤╒б╓А', - goShopping: '╘▒╒╢╓Б╒╢╒е╒м ╒г╒╢╒╕╓В╒┤╒╢╒е╓А╒л', - total: '╘╕╒╢╒д╒б╒┤╒е╒╢╒и', - items: '╘▒╒║╓А╒б╒╢╓Д╒╢╒е╓А', - deliveryLabel: '╘▒╒╝╒б╓Д╒╕╓В╒┤', - toPay: '╒О╒│╒б╓А╒┤╒б╒╢ ╒е╒╢╒й╒б╒п╒б', - agreeWith: '╘╡╒╜ ╒░╒б╒┤╒б╒▒╒б╒╡╒╢ ╒е╒┤', - publicOffer: '╒░╒б╒╢╓А╒б╒╡╒л╒╢ ╓Е╓Ж╒е╓А╒┐╒б╒╡╒л╒╢', - returnPolicy: '╒╛╒е╓А╒б╒д╒б╓А╒▒╒л ╓Д╒б╒▓╒б╓Д╒б╒п╒б╒╢╒╕╓В╒й╒╡╒б╒╢╒и', - guaranteeTerms: '╒е╓А╒б╒╖╒н╒л╓Д╒б╒╡╒л╒╢ ╒║╒б╒╡╒┤╒б╒╢╒╢╒е╓А╒л╒╢', - privacyPolicy: '╒г╒б╒▓╒┐╒╢╒л╒╕╓В╒й╒╡╒б╒╢ ╓Д╒б╒▓╒б╓Д╒б╒п╒б╒╢╒╕╓В╒й╒╡╒б╒╢╒и', - and: '╓З', - checkout: '╒Б╓З╒б╒п╒е╓А╒║╒е╒м ╒║╒б╒┐╒╛╒е╓А', - close: '╒У╒б╒п╒е╒м', - creatingPayment: '╒О╒│╒б╓А╒╕╓В╒┤╒и ╒╜╒┐╒е╒▓╒о╒╛╒╕╓В╒┤ ╒з...', - waitFewSeconds: '╒Н╒║╒б╒╜╒е╓Д ╒┤╒л ╓Д╒б╒╢╒л ╒╛╒б╒╡╓А╒п╒╡╒б╒╢', - scanQr: '╒Н╒п╒б╒╢╒б╒╛╒╕╓А╒е╓Д QR ╒п╒╕╒д╒и ╒╛╒│╒б╓А╒┤╒б╒╢ ╒░╒б╒┤╒б╓А', - amountToPay: '╒О╒│╒б╓А╒┤╒б╒╢ ╒г╒╕╓В╒┤╒б╓А╒и╒Э', - waitingPayment: '╒Н╒║╒б╒╜╒╕╓В╒┤ ╒е╒╢╓Д ╒╛╒│╒б╓А╒┤╒б╒╢╒и...', - copied: 'тЬУ ╒К╒б╒┐╒│╒е╒╢╒╛╒б╒о ╒з', - copyLink: '╒К╒б╒┐╒│╒е╒╢╒е╒м ╒░╒▓╒╕╓В╒┤╒и', - openNewTab: '╘▓╒б╓Б╒е╒м ╒╢╒╕╓А ╒╢╒е╓А╒д╒л╓А╒╕╓В╒┤', - paymentSuccess: '╒З╒╢╒╕╓А╒░╒б╒╛╒╕╓А╒╕╓В╒┤ ╒е╒╢╓Д╓Й ╒О╒│╒б╓А╒╕╓В╒┤╒и ╒░╒б╒╗╒╕╒▓╒╕╓В╒й╒╡╒б╒┤╒в ╒п╒б╒┐╒б╓А╒╛╒е╒м ╒з╓Й', - paymentSuccessDesc: '╒Д╒╕╓В╒┐╓Д╒б╒г╓А╒е╓Д ╒▒╒е╓А ╒п╒╕╒╢╒┐╒б╒п╒┐╒б╒╡╒л╒╢ ╒┐╒╛╒╡╒б╒м╒╢╒е╓А╒и, ╓З ╒┤╒е╒╢╓Д ╒п╒╕╓В╒▓╒б╓А╒п╒е╒╢╓Д ╒г╒╢╒╕╓В╒┤╒и ╒┤╒л ╓Д╒б╒╢╒л ╓А╒╕╒║╒е╒л ╒и╒╢╒й╒б╓Б╓Д╒╕╓В╒┤', - sending: '╒И╓В╒▓╒б╓А╒п╒╛╒╕╓В╒┤ ╒з...', - send: '╒И╓В╒▓╒б╓А╒п╒е╒м', - paymentTimeout: '╒Н╒║╒б╒╜╒┤╒б╒╢ ╒к╒б╒┤╒б╒╢╒б╒п╒и ╒╜╒║╒б╒╝╒╛╒е╒м ╒з', - paymentTimeoutDesc: '╒Д╒е╒╢╓Д ╒╣╒е╒╢╓Д ╒╜╒┐╒б╓Б╒е╒м ╒╛╒│╒б╓А╒┤╒б╒╢ ╒░╒б╒╜╒┐╒б╒┐╒╕╓В╒┤ 3 ╓А╒╕╒║╒е╒л ╒и╒╢╒й╒б╓Б╓Д╒╕╓В╒┤╓Й', - autoClose: '╒К╒б╒┐╒╕╓В╒░╒б╒╢╒и ╒п╓Г╒б╒п╒╛╒л ╒б╒╛╒┐╒╕╒┤╒б╒┐...', - confirmClear: '╒А╒б╒┤╒╕╒ж╒╛╒б╒Ю╒о ╒е╓Д, ╒╕╓А ╓Б╒б╒╢╒п╒б╒╢╒╕╓В╒┤ ╒е╓Д ╒┤╒б╓Д╓А╒е╒м ╒ж╒б╒┤╒в╒╡╒╕╓В╒▓╒и╓Й', - acceptTerms: '╘╜╒╢╒д╓А╒╕╓В╒┤ ╒е╒╢╓Д ╒и╒╢╒д╒╕╓В╒╢╒е╒м ╓Е╓Ж╒е╓А╒┐╒б╒╡╒л, ╒╛╒е╓А╒б╒д╒б╓А╒▒╒л ╓З ╒е╓А╒б╒╖╒н╒л╓Д╒л ╒║╒б╒╡╒┤╒б╒╢╒╢╒е╓А╒и ╒║╒б╒┐╒╛╒е╓А╒и ╒░╒б╒╜╒┐╒б╒┐╒е╒м╒╕╓В ╒░╒б╒┤╒б╓А╓Й', - copyError: '╒К╒б╒┐╒│╒е╒╢╒┤╒б╒╢ ╒╜╒н╒б╒м╒Э', - emailSuccess: 'Email-╒и ╒░╒б╒╗╒╕╒▓╒╕╓В╒й╒╡╒б╒┤╒в ╒╕╓В╒▓╒б╓А╒п╒╛╒е╒м ╒з╓Й ╒Н╒┐╒╕╓В╒г╒е╓Д ╒▒╒е╓А ╓Г╒╕╒╜╒┐╒и╓Й', - emailError: 'Email ╒╕╓В╒▓╒б╓А╒п╒е╒м╒╕╓В ╒к╒б╒┤╒б╒╢╒б╒п ╒┐╒е╒▓╒л ╒╕╓В╒╢╒е╓Б╒б╒╛ ╒╜╒н╒б╒м╓Й ╘╜╒╢╒д╓А╒╕╓В╒┤ ╒е╒╢╓Д ╓Г╒╕╓А╒▒╒е╒м ╒п╓А╒п╒л╒╢╓Й', - phoneRequired: '╒А╒е╒╝╒б╒н╒╕╒╜╒б╒░╒б╒┤╒б╓А╒и ╒║╒б╓А╒┐╒б╒д╒л╓А ╒з', - phoneMoreDigits: '╒Д╒╕╓В╒┐╓Д╒б╒г╓А╒е╓Д ╓З╒╜ {{count}} ╒й╒л╒╛', - phoneTooMany: '╒Й╒б╓Г╒б╒ж╒б╒╢╓Б ╒╖╒б╒┐ ╒й╒╛╒е╓А', - emailRequired: 'Email-╒и ╒║╒б╓А╒┐╒б╒д╒л╓А ╒з', - emailTooShort: 'Email-╒и ╒╣╒б╓Г╒б╒ж╒б╒╢╓Б ╒п╒б╓А╒│ ╒з (╒╢╒╛╒б╒ж╒б╒г╒╕╓В╒╡╒╢╒и 5 ╒╢╒л╒╖)', - emailTooLong: 'Email-╒и ╒╣╒б╓Г╒б╒ж╒б╒╢╓Б ╒е╓А╒п╒б╓А ╒з (╒б╒╝╒б╒╛╒е╒м╒б╒г╒╕╓В╒╡╒╢╒и 100 ╒╢╒л╒╖)', - emailNeedsAt: 'Email-╒и ╒║╒е╒┐╓Д ╒з ╒║╒б╓А╒╕╓В╒╢╒б╒п╒л @ ╒╢╒╖╒б╒╢╒и', - emailNeedsDomain: 'Email-╒и ╒║╒е╒┐╓Д ╒з ╒║╒б╓А╒╕╓В╒╢╒б╒п╒л ╒д╒╕╒┤╒е╒╢ (.com, .ru ╓З ╒б╒╡╒м╒╢)', - emailInvalid: 'Email-╒л ╒▒╓З╒б╒╣╒б╓Г╒и ╒╜╒н╒б╒м ╒з', + title: 'Զամբյուղ', + clear: 'Մաքրել', + empty: 'Զամբյուղը դատարկ է', + emptyDesc: 'Ավելացրեք ապրանքներ՝ գնումները սկսելու համար', + goShopping: 'Գնալ գնումների', + total: 'Ընդամենը', + items: 'Ապրանքներ', + deliveryLabel: 'Առաքում', + toPay: 'Վճարման ենթակա', + agreeWith: 'Ես համաձայն եմ', + publicOffer: 'հանրային օֆերտայի', + returnPolicy: 'վերադարձի քաղաքականության', + guaranteeTerms: 'երաշխիքային պայմանների', + privacyPolicy: 'գաղտնիության քաղաքականության', + and: 'և', + checkout: 'Ձևակերպել պատվերը', + close: 'Փակել', + creatingPayment: 'Վճարման ստեղծում...', + waitFewSeconds: 'Խնդրում ենք սպասել մի քանի վայրկյան', + scanQr: 'Սքանավորեք QR կոդը վճարման համար', + amountToPay: 'Վճարման գումար՝', + waitingPayment: 'Սպասում ենք վճարմանը...', + copied: '✓ Պատճենված է', + copyLink: 'Պատճենել հղումը', + openNewTab: 'Բացել նոր ներդիրում', + paymentSuccess: 'Շնորհավորում ենք! Վճարումը հաջող է անցել!', + paymentSuccessDesc: 'Մուտքագրեք ձեր տվյալները, և մենք կուղարկենք գնումը մի քանի րոպեի ընթացքում', + sending: 'Ուղարկվում է...', + send: 'Ուղարկել', + paymentTimeout: 'Ժամանակը սպառվեց', + paymentTimeoutDesc: 'Մենք չստացանք վճարման հաստատում 3 րոպեի ընթացքում։', + autoClose: 'Պատուհանը կփակվի ավտոմատ...', + confirmClear: 'Վստա՞հ եք, որ ցանկանում եք մաքրել զամբյուղը', + acceptTerms: 'Խնդրում ենք ընդունել պայմանները՝ պատվերը հաստատելու համար։', + copyError: 'Պատճենման սխալ՝', + emailSuccess: 'Email-ը հաջողությամբ ուղարկվեց։ Ստուգեք ձեր փոստը։', + emailError: 'Սխալ email ուղարկելիս։ Խնդրում ենք փորձել կրկին։', + phoneRequired: 'Հեռախոսահամարը պարտադիր է', + phoneMoreDigits: 'Մուտքագրեք ևս {{count}} թիվ', + phoneTooMany: 'Չափազանց շատ թվեր', + emailRequired: 'Email-ը պարտադիր է', + emailTooShort: 'Email-ը չափազանց կարճ է (առնվազն 5 նիշ)', + emailTooLong: 'Email-ը չափազանց երկար է (առավելագույնը 100 նիշ)', + emailNeedsAt: 'Email-ը պետք է պարունակի @', + emailNeedsDomain: 'Email-ը պետք է պարունակի դոմեյն (.com, .ru և այլն)', + emailInvalid: 'Սխալ email ձևաչափ', + loginRequired: 'Մուտք գործեք ձևակերպելու համար', + loginRequiredDesc: 'Պատվեր ձևակերպելու համար մուտք գործեք Telegram-ով', + loginWithTelegram: 'Մուտք Telegram-ով', + orScanQr: 'Կամ սքանավորեք QR կոդը', }, search: { - title: '╘▒╒║╓А╒б╒╢╓Д╒╢╒е╓А╒л ╒╕╓А╒╕╒╢╒╕╓В╒┤', - placeholder: '╒Д╒╕╓В╒┐╓Д╒б╒г╓А╒е╓Д ╒б╒║╓А╒б╒╢╓Д╒л ╒б╒╢╒╕╓В╒╢╒и...', - resultsCount: '╘│╒┐╒╢╒╛╒б╒о ╒б╒║╓А╒б╒╢╓Д╒╢╒е╓А╒Э', - searching: '╒И╓А╒╕╒╢╒╕╓В╒┤...', - retry: '╒У╒╕╓А╒▒╒е╒м ╒п╓А╒п╒л╒╢', - noResults: '╒И╒╣╒л╒╢╒╣ ╒╣╒л ╒г╒┐╒╢╒╛╒е╒м', - noResultsFor: '"{{query}}" ╒░╒б╓А╓Б╒┤╒б╒╢ ╒░╒б╒┤╒б╓А ╒б╒║╓А╒б╒╢╓Д╒╢╒е╓А ╒╣╒е╒╢ ╒г╒┐╒╢╒╛╒е╒м', - noResultsHint: '╒У╒╕╓А╒▒╒е╓Д ╓Г╒╕╒н╒е╒м ╒░╒б╓А╓Б╒╕╓В╒┤╒и ╒п╒б╒┤ ╓Е╒г╒┐╒б╒г╒╕╓А╒о╒е╒м ╒б╒╡╒м ╒в╒б╒╢╒б╒м╒л ╒в╒б╒╝╒е╓А', - addToCart: '╘▒╒╛╒е╒м╒б╓Б╒╢╒е╒м ╒ж╒б╒┤╒в╒╡╒╕╓В╒▓', - loadingMore: '╘▓╒е╒╝╒╢╒╛╒╕╓В╒┤ ╒з...', - allLoaded: '╘▓╒╕╒м╒╕╓А ╒б╓А╒д╒╡╒╕╓В╒╢╓Д╒╢╒е╓А╒и ╒в╒е╒╝╒╢╒╛╒б╒о ╒е╒╢', - emptyState: '╒Д╒╕╓В╒┐╓Д╒б╒г╓А╒е╓Д ╒░╒б╓А╓Б╒╕╓В╒┤ ╒б╒║╓А╒б╒╢╓Д╒╢╒е╓А ╒╕╓А╒╕╒╢╒е╒м╒╕╓В ╒░╒б╒┤╒б╓А', - of: '╒л╓Б', + title: 'Ապրանքների որոնում', + placeholder: 'Մուտքագրեք ապրանքի անվանումը...', + resultsCount: 'Գտնված ապրանքներ՝', + searching: 'Որոնում...', + retry: 'Փորձել կրկին', + noResults: 'Ոչինչ չի գտնվել', + noResultsFor: '"{{query}}" հարցմամբ ապրանքներ չեն գտնվել', + noResultsHint: 'Փորձեք փոխել հարցումը կամ օգտագործել այլ բանալի բառեր', + addToCart: 'Ավելացնել զամբյուղ', + loadingMore: 'Բեռնում...', + allLoaded: 'Բոլոր արդյունքները բեռնված են', + emptyState: 'Մուտքագրեք հարցում որոնման համար', + of: '-ից', }, category: { - retry: '╒У╒╕╓А╒▒╒е╒м ╒п╓А╒п╒л╒╢', - addToCart: '╘▒╒╛╒е╒м╒б╓Б╒╢╒е╒м ╒ж╒б╒┤╒в╒╡╒╕╓В╒▓', - loadingMore: '╘▓╒е╒╝╒╢╒╛╒╕╓В╒┤ ╒з...', - allLoaded: '╘▓╒╕╒м╒╕╓А ╒б╒║╓А╒б╒╢╓Д╒╢╒е╓А╒и ╒в╒е╒╝╒╢╒╛╒б╒о ╒е╒╢', - emptyTitle: '╒И╓В╒║╒╜╓Й ╘▒╒╡╒╜╒┐╒е╒▓ ╒д╒е╒╝ ╒д╒б╒┐╒б╓А╒п ╒з', - emptyDesc: '╘▒╒╡╒╜ ╒п╒б╒┐╒е╒г╒╕╓А╒л╒б╒╡╒╕╓В╒┤ ╒д╒е╒╝ ╒б╒║╓А╒б╒╢╓Д╒╢╒е╓А ╒╣╒п╒б╒╢, ╒в╒б╒╡╓Б ╒╖╒╕╓В╒┐╒╕╒╛ ╒п╒░╒б╒╡╒┐╒╢╒╛╒е╒╢', - goHome: '╘│╒м╒н╒б╒╛╒╕╓А ╒з╒╗', - loading: '╘▒╒║╓А╒б╒╢╓Д╒╢╒е╓А╒и ╒в╒е╒╝╒╢╒╛╒╕╓В╒┤ ╒е╒╢...', + retry: 'Փորձել կրկին', + addToCart: 'Ավելացնել զամբյուղ', + loadingMore: 'Բեռնում...', + allLoaded: 'Բոլոր ապրանքները բեռնված են', + emptyTitle: 'Վա՜յ, այստեղ դեռ դատարկ է', + emptyDesc: 'Այս կատեգորիայում դեռ ապրանքներ չկան', + goHome: 'Գլխավոր', + loading: 'Ապրանքների բեռնում...', }, subcategories: { - loading: '╘╡╒╢╒й╒б╒п╒б╒┐╒е╒г╒╕╓А╒л╒б╒╢╒е╓А╒и ╒в╒е╒╝╒╢╒╛╒╕╓В╒┤ ╒е╒╢...', - retry: '╒У╒╕╓А╒▒╒е╒м ╒п╓А╒п╒л╒╢', - emptyTitle: '╒И╓В╒║╒╜╓Й ╘╡╒╢╒й╒б╒п╒б╒┐╒е╒г╒╕╓А╒л╒б╒╢╒е╓А ╒д╒е╒╝ ╒╣╒п╒б╒╢', - emptyDesc: '╘▒╒╡╒╜ ╒в╒б╒к╒╢╒╕╓В╒┤ ╒д╒е╒╝ ╒е╒╢╒й╒б╒п╒б╒┐╒е╒г╒╕╓А╒л╒б╒╢╒е╓А ╒╣╒п╒б╒╢, ╒в╒б╒╡╓Б ╒╖╒╕╓В╒┐╒╕╒╛ ╒п╒░╒б╒╡╒┐╒╢╒╛╒е╒╢', - goHome: '╘│╒м╒н╒б╒╛╒╕╓А ╒з╒╗', + loading: 'Ենթակատեգորիաների բեռնում...', + retry: 'Փորձել կրկին', + emptyTitle: 'Ենթակատեգորիաներ չկան', + emptyDesc: 'Այս բաժնում դեռ ենթակատեգորիաներ չկան', + goHome: 'Գլխավոր', + itemsInCategory: 'Ապրանքներ այս կատեգորիայում', }, itemDetail: { - loading: '╘▓╒е╒╝╒╢╒╛╒╕╓В╒┤ ╒з...', - loadingDexar: '╘▒╒║╓А╒б╒╢╓Д╒и ╒в╒е╒╝╒╢╒╛╒╕╓В╒┤ ╒з...', - back: '╒О╒е╓А╒б╒д╒б╒╝╒╢╒б╒м', - backHome: '╒О╒е╓А╒б╒д╒б╒╝╒╢╒б╒м ╒г╒м╒н╒б╒╛╒╕╓А ╒з╒╗', - noImage: '╒К╒б╒┐╒п╒е╓А ╒╣╒п╒б', - stock: '╘▒╒╝╒п╒б╒╡╒╕╓В╒й╒╡╒╕╓В╒╢╒Э', - inStock: '╘▒╒╝╒п╒б ╒з', - lowStock: '╒Д╒╢╒б╓Б╒е╒м ╒з ╓Д╒л╒╣', - lastItems: '╒О╒е╓А╒╗╒л╒╢ ╒░╒б╒┐╒е╓А╒и', - mediumStock: '╒О╒е╓А╒╗╒б╒╢╒╕╓В╒┤ ╒з', - addToCart: '╘▒╒╛╒е╒м╒б╓Б╒╢╒е╒м ╒ж╒б╒┤╒в╒╡╒╕╓В╒▓', - description: '╒Ж╒п╒б╓А╒б╒г╓А╒╕╓В╒й╒╡╒╕╓В╒╢', - specifications: 'u{0532}u{0576}u{0578}u{0582}u{0569}u{0561}u{0563}u{0580}u{0565}u{0580}', - reviews: '╘┐╒б╓А╒о╒л╓Д╒╢╒е╓А', - yourReview: '╒Б╒е╓А ╒п╒б╓А╒о╒л╓Д╒и', - leaveReview: '╘╣╒╕╒▓╒╢╒е╒м ╒п╒б╓А╒о╒л╓Д', - rating: '╘│╒╢╒б╒░╒б╒┐╒б╒п╒б╒╢╒Э', - reviewPlaceholder: '╘┐╒л╒╜╒╛╒е╓Д ╒▒╒е╓А ╒┐╒║╒б╒╛╒╕╓А╒╕╓В╒й╒╡╒╕╓В╒╢╒╢╒е╓А╒╕╒╛ ╒б╒║╓А╒б╒╢╓Д╒л ╒┤╒б╒╜╒л╒╢...', - reviewPlaceholderDexar: '╘┐╒л╒╜╒╛╒е╓Д ╒▒╒е╓А ╒┐╒║╒б╒╛╒╕╓А╒╕╓В╒й╒╡╒╕╓В╒╢╒╢╒е╓А╒╕╒╛...', - anonymous: '╘▒╒╢╒б╒╢╒╕╓В╒╢', - submitting: '╒И╓В╒▓╒б╓А╒п╒╛╒╕╓В╒┤ ╒з...', - submit: '╒И╓В╒▓╒б╓А╒п╒е╒м', - reviewSuccess: '╒З╒╢╒╕╓А╒░╒б╒п╒б╒м╒╕╓В╒й╒╡╒╕╓В╒╢ ╒▒╒е╓А ╒п╒б╓А╒о╒л╓Д╒л ╒░╒б╒┤╒б╓А╓Й', - reviewError: '╒И╓В╒▓╒б╓А╒п╒┤╒б╒╢ ╒╜╒н╒б╒м╓Й ╒У╒╕╓А╒▒╒е╓Д ╒б╒╛╒е╒м╒л ╒╕╓В╒╖╓Й', - defaultUser: '╒Х╒г╒┐╒б╒┐╒е╓А', - defaultUserDexar: '╘▒╒╢╒б╒╢╒╕╓В╒╢', - noReviews: '╘┤╒е╒╝ ╒п╒б╓А╒о╒л╓Д╒╢╒е╓А ╒╣╒п╒б╒╢╓Й ╘┤╒б╓А╒▒╒е╓Д ╒б╒╝╒б╒╗╒л╒╢╒и╓Й', - qna: '╒А╒б╓А╓Б╒е╓А ╓З ╒║╒б╒┐╒б╒╜╒н╒б╒╢╒╢╒е╓А', - photo: '╘╝╒╕╓В╒╜╒б╒╢╒п╒б╓А', - reviewsCount: '╒п╒б╓А╒о╒л╓Д', - today: '╘▒╒╡╒╜╓Е╓А', - yesterday: '╘╡╓А╒е╒п', - daysAgo: '╓Е╓А ╒б╒╝╒б╒╗', - weeksAgo: '╒╖╒б╒в╒б╒й ╒б╒╝╒б╒╗', + loading: 'Բեռնում...', + loadingDexar: 'Ապրանքի բեռնում...', + back: 'Վերադառնալ', + backHome: 'Վերադառնալ գլխավոր էջ', + noImage: 'Պատկեր չկա', + stock: 'Առկայություն՝', + inStock: 'Առկա է', + lowStock: 'Քիչ է մնացել', + lastItems: 'Վերջին հատերը', + mediumStock: 'Ավարտվում է', + addToCart: 'Ավելացնել զամբյուղ', + description: 'Նկարագրություն', + specifications: 'Բնութագրեր', + reviews: 'Կարծիքներ', + yourReview: 'Ձեր կարծիքը', + leaveReview: 'Թողնել կարծիք', + rating: 'Գնահատական՝', + reviewPlaceholder: 'Կիսվեք ձեր կարծիքով...', + reviewPlaceholderDexar: 'Կիսվեք տպավորություններով...', + anonymous: 'Անանուն', + submitting: 'Ուղարկվում է...', + submit: 'Ուղարկել', + reviewSuccess: 'Շնորհակալություն ձեր կարծիքի համար!', + reviewError: 'Սխալ ուղարկելիս։ Փորձեք ավելի ուշ։', + defaultUser: 'Օգտատեր', + defaultUserDexar: 'Անանուն', + noReviews: 'Կարծիքներ դեռ չկան', + qna: 'Հարցեր և պատասխաններ', + photo: 'Լուսանկար', + reviewsCount: 'կարծիք', + today: 'Այսօր', + yesterday: 'Երեկ', + daysAgo: 'օր առաջ', + weeksAgo: 'շաբաթ առաջ', + colour: 'Գույն', + size: 'Չափ', }, app: { - connecting: '╒Д╒л╒б╓Б╒╕╓В╒┤ ╒╜╒е╓А╒╛╒е╓А╒л╒╢...', - serverUnavailable: '╒Н╒е╓А╒╛╒е╓А╒и ╒░╒б╒╜╒б╒╢╒е╒м╒л ╒╣╒з', - serverError: '╒Й╒░╒б╒╗╒╕╒▓╒╛╒е╓Б ╒┤╒л╒б╒╢╒б╒м ╒╜╒е╓А╒╛╒е╓А╒л╒╢╓Й ╒Н╒┐╒╕╓В╒г╒е╓Д ╒л╒╢╒┐╒е╓А╒╢╒е╒┐ ╒п╒б╒║╒и╓Й', - retryConnection: '╘┐╓А╒п╒╢╒е╒м ╓Г╒╕╓А╒▒╒и', - pageTitle: '╘▒╒║╓А╒б╒╢╓Д╒╢╒е╓А╒л ╓З ╒о╒б╒╝╒б╒╡╒╕╓В╒й╒╡╒╕╓В╒╢╒╢╒е╓А╒л ╒┤╒б╓А╓Д╒е╒й╓Г╒м╒е╒╡╒╜', + connecting: 'Կապ սերվերի հետ...', + serverUnavailable: 'Սերվերը անհասանելի է', + serverError: 'Չհաջողվեց միանալ սերվերին։ Ստուգեք ինտերնետը։', + retryConnection: 'Փորձել կրկին', + pageTitle: 'Ապրանքների և ծառայությունների մարքեթփլեյս', }, carousel: { - loading: '╘▒╒║╓А╒б╒╢╓Д╒╢╒е╓А╒и ╒в╒е╒╝╒╢╒╛╒╕╓В╒┤ ╒е╒╢...', - addToCart: '╘▒╒╛╒е╒м╒б╓Б╒╢╒е╒м ╒ж╒б╒┤╒в╒╡╒╕╓В╒▓', + loading: 'Ապրանքների բեռնում...', + addToCart: 'Ավելացնել զամբյուղ', }, common: { - retry: '╒У╒╕╓А╒▒╒е╒м ╒п╓А╒п╒л╒╢', - loading: '╘▓╒е╒╝╒╢╒╛╒╕╓В╒┤ ╒з...', + retry: 'Փորձել կրկին', + loading: 'Բեռնում...', }, - location: { - allRegions: 'Բոլոր տարածաշրջաններ', - chooseRegion: 'Ընտրեք տարածաշրջան', - detectAuto: 'Որոշել ինքնաշխատ', + allRegions: 'Բոլոր տարածաշրջանները', + chooseRegion: 'Ընտրեք տարածաշրջանը', + detectAuto: 'Որոշել ավտոմատ', }, auth: { - loginRequired: 'Մուտք պահանջվում է', - loginDescription: 'Պատվերի կատարման համար մուտք արեք Telegram-ի միջոցով', - checking: 'Ստուգում է...', - loginWithTelegram: 'Մուտք գործել Telegram-ով', - orScanQr: 'Կամ սկանավորեք QR կոդը', - loginNote: 'Մուտքից հետո դուք կվերադառնավեք', + loginRequired: 'Պահանջվում է մուտք', + loginDescription: 'Պատվերի համար մուտք գործեք Telegram-ով', + checking: 'Ստուգում...', + loginWithTelegram: 'Մուտք Telegram-ով', + orScanQr: 'Կամ սքանավորեք QR կոդը', + loginNote: 'Մուտքից հետո դուք կվերաուղղվեք', }, }; diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index c8679da..7e8c97c 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -102,6 +102,10 @@ export const ru: Translations = { emailNeedsAt: 'Email должен содержать @', emailNeedsDomain: 'Email должен содержать домен (.com, .ru и т.д.)', emailInvalid: 'Некорректный формат email', + loginRequired: 'Войдите для оформления', + loginRequiredDesc: 'Для оформления заказа войдите через Telegram', + loginWithTelegram: 'Войти через Telegram', + orScanQr: 'Или отсканируйте QR-код', }, search: { title: 'Поиск товаров', @@ -134,6 +138,7 @@ export const ru: Translations = { emptyTitle: 'Упс! Подкатегорий пока нет', emptyDesc: 'В этом разделе ещё нет подкатегорий, но скоро они появятся', goHome: 'На главную', + itemsInCategory: 'Товары в этой категории', }, itemDetail: { loading: 'Загрузка...', @@ -170,6 +175,8 @@ export const ru: Translations = { yesterday: 'Вчера', daysAgo: 'дн. назад', weeksAgo: 'нед. назад', + colour: 'Цвет', + size: 'Размер', }, app: { connecting: 'Подключение к серверу...', diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index 7ced44d..b707b98 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -100,6 +100,10 @@ export interface Translations { emailNeedsAt: string; emailNeedsDomain: string; emailInvalid: string; + loginRequired: string; + loginRequiredDesc: string; + loginWithTelegram: string; + orScanQr: string; }; search: { title: string; @@ -132,6 +136,7 @@ export interface Translations { emptyTitle: string; emptyDesc: string; goHome: string; + itemsInCategory: string; }; itemDetail: { loading: string; @@ -168,6 +173,8 @@ export interface Translations { yesterday: string; daysAgo: string; weeksAgo: string; + colour: string; + size: string; }; app: { connecting: string; diff --git a/src/app/interceptors/api-headers.interceptor.ts b/src/app/interceptors/api-headers.interceptor.ts index 0775e40..3336170 100644 --- a/src/app/interceptors/api-headers.interceptor.ts +++ b/src/app/interceptors/api-headers.interceptor.ts @@ -2,35 +2,48 @@ import { HttpInterceptorFn } from '@angular/common/http'; import { inject } from '@angular/core'; import { LocationService } from '../services/location.service'; import { LanguageService } from '../services/language.service'; +import { AuthService } from '../services/auth.service'; import { environment } from '../../environments/environment'; -/** - * Interceptor that attaches X-Region and X-Language headers - * to every outgoing request aimed at our API. - * - * The backend reads these headers to: - * - filter catalog by region - * - return translated content in the requested language - */ +/** Map internal language codes to API header values */ +const LANG_HEADER_MAP: Record = { + 'ru': 'RU', + 'en': 'EN', + 'hy': 'AM', +}; + +/** Map region IDs to API header values */ +const REGION_HEADER_MAP: Record = { + 'moscow': 'Moscow', + 'spb': 'ST. Petersburg', + 'yerevan': 'Yerevan', +}; + export const apiHeadersInterceptor: HttpInterceptorFn = (req, next) => { - // Only attach headers to our own API requests if (!req.url.startsWith(environment.apiUrl)) { return next(req); } const locationService = inject(LocationService); const languageService = inject(LanguageService); + const authService = inject(AuthService); - const regionId = locationService.regionId(); // '' when global - const lang = languageService.currentLanguage(); // 'ru' | 'en' | 'hy' + const regionId = locationService.regionId(); + const lang = languageService.currentLanguage(); + const currency = languageService.currentCurrency(); + const session = authService.session(); let headers = req.headers; if (regionId) { - headers = headers.set('X-Region', regionId); + headers = headers.set('X-Region', REGION_HEADER_MAP[regionId] ?? regionId); } if (lang) { - headers = headers.set('X-Language', lang); + headers = headers.set('X-Language', LANG_HEADER_MAP[lang] ?? lang.toUpperCase()); + } + headers = headers.set('Currency', currency || 'RUB'); + if (session?.sessionId) { + headers = headers.set('WebSessionID', session.sessionId); } return next(req.clone({ headers })); diff --git a/src/app/interceptors/mock-data.interceptor.ts b/src/app/interceptors/mock-data.interceptor.ts index 6bfd703..0457d81 100644 --- a/src/app/interceptors/mock-data.interceptor.ts +++ b/src/app/interceptors/mock-data.interceptor.ts @@ -167,6 +167,22 @@ const MOCK_ITEMS: any[] = [ ], tags: ['new', 'featured', 'apple'], badges: ['new', 'bestseller'], + colour: 'Натуральный титан', + size: '', + names: [ + { language: 'ru', value: 'iPhone 15 Pro Max' }, + { language: 'en', value: 'iPhone 15 Pro Max' }, + { language: 'hy', value: 'iPhone 15 Pro Max' } + ], + descriptions: [ + { language: 'ru', value: 'Новейший iPhone с титановым корпусом и чипом A17 Pro' }, + { language: 'en', value: 'Latest iPhone with titanium body and A17 Pro chip' } + ], + attributes: [ + { key: 'Цвет', value: 'Натуральный титан' }, + { key: 'Память', value: '256 ГБ' }, + { key: 'Процессор', value: 'A17 Pro' } + ], simpleDescription: 'Новейший iPhone с титановым корпусом и чипом A17 Pro', description: [ { key: 'Цвет', value: 'Натуральный титан' }, @@ -230,6 +246,20 @@ const MOCK_ITEMS: any[] = [ ], tags: ['new', 'android', 'samsung'], badges: ['new', 'sale'], + colour: 'Титановый серый', + size: '', + names: [ + { language: 'ru', value: 'Samsung Galaxy S24 Ultra' }, + { language: 'en', value: 'Samsung Galaxy S24 Ultra' } + ], + descriptions: [ + { language: 'ru', value: 'Премиальный флагман Samsung с S Pen' }, + { language: 'en', value: 'Premium Samsung flagship with S Pen' } + ], + attributes: [ + { key: 'Память', value: '512 ГБ' }, + { key: 'ОЗУ', value: '12 ГБ' } + ], simpleDescription: 'Премиальный флагман Samsung с S Pen', description: [ { key: 'Цвет', value: 'Титановый серый' }, diff --git a/src/app/models/item.model.ts b/src/app/models/item.model.ts index 9293de6..dfcf5c7 100644 --- a/src/app/models/item.model.ts +++ b/src/app/models/item.model.ts @@ -42,6 +42,24 @@ export interface Question { downvotes: number; } +/** Localized name entry from backend */ +export interface ItemName { + language: string; + value: string; +} + +/** Localized description entry from backend */ +export interface ItemDescription { + language: string; + value: string; +} + +/** Key-value attribute pair */ +export interface ItemAttribute { + key: string; + value: string; +} + export interface Item { categoryID: number; itemID: number; @@ -55,9 +73,16 @@ export interface Item { rating: number; callbacks: Review[] | null; questions: Question[] | null; - partnerID?: string; quantity?: number; + // Backend API fields + colour?: string; + size?: string; + language?: string; + names?: ItemName[]; + descriptions?: ItemDescription[]; + attributes?: ItemAttribute[]; + // BackOffice API fields id?: string; visible?: boolean; diff --git a/src/app/pages/cart/cart.component.html b/src/app/pages/cart/cart.component.html index 629cea1..90c143d 100644 --- a/src/app/pages/cart/cart.component.html +++ b/src/app/pages/cart/cart.component.html @@ -31,12 +31,12 @@ (touchstart)="onSwipeStart(item.itemID, $event)">
- +
- {{ item.name }} + {{ itemName(item) }}
-

{{ item.simpleDescription || item?.description?.substring?.(0, 100) || '' }}...

+

{{ itemDesc(item) || '' }}...

+ + @if (item.colour || item.size) { +
+ @if (item.colour) { + {{ 'itemDetail.colour' | translate }}: {{ item.colour }} + } + @if (item.size) { + {{ 'itemDetail.size' | translate }}: {{ item.size }} + } +
+ } @if (item.badges && item.badges.length > 0) {
@@ -58,11 +69,11 @@
@if (item.discount > 0) {
- {{ item.price }} ₽ - {{ getDiscountedPrice(item) | number:'1.2-2' }} ₽ + {{ item.price }} {{ item.currency }} + {{ getDiscountedPrice(item) | number:'1.2-2' }} {{ item.currency }}
} @else { - {{ item.price }} ₽ + {{ item.price }} {{ item.currency }} }
@@ -99,17 +110,17 @@
{{ 'cart.items' | translate }} ({{ itemCount() }}) - {{ totalPrice() | number:'1.2-2' }} ₽ + {{ totalPrice() | number:'1.2-2' }} {{ currentCurrency }}
{{ 'cart.deliveryLabel' | translate }} - 0 ₽ + 0 {{ currentCurrency }}
{{ 'cart.toPay' | translate }} - {{ totalPrice() | number:'1.2-2' }} ₽ + {{ totalPrice() | number:'1.2-2' }} {{ currentCurrency }}
@@ -138,6 +149,36 @@ > {{ 'cart.checkout' | translate }} + + @if (!isAuthenticated()) { + + }
} @@ -174,7 +215,7 @@
{{ 'cart.amountToPay' | translate }} - {{ totalPrice() | number:'1.2-2' }} RUB + {{ totalPrice() | number:'1.2-2' }} {{ currentCurrency }}
@@ -264,3 +305,5 @@
} + + diff --git a/src/app/pages/cart/cart.component.scss b/src/app/pages/cart/cart.component.scss index 38040e2..9c75ba6 100644 --- a/src/app/pages/cart/cart.component.scss +++ b/src/app/pages/cart/cart.component.scss @@ -364,6 +364,22 @@ line-height: 1.5; } + .cart-item-variants { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-top: 4px; + + .cart-variant { + font-size: 0.8rem; + color: #497671; + background: rgba(73, 118, 113, 0.08); + padding: 3px 10px; + border-radius: 6px; + font-weight: 500; + } + } + .item-footer { display: flex; justify-content: space-between; @@ -464,6 +480,22 @@ line-height: 1.6; } + .cart-item-variants { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-top: 4px; + + .cart-variant { + font-size: 0.8rem; + color: #6366f1; + background: rgba(99, 102, 241, 0.08); + padding: 3px 10px; + border-radius: 6px; + font-weight: 500; + } + } + .item-footer { display: flex; justify-content: space-between; @@ -689,6 +721,85 @@ cursor: not-allowed; } } + + .cart-login-gate { + margin-top: 16px; + padding: 20px; + border-radius: 14px; + background: rgba(42, 171, 238, 0.05); + border: 1px dashed rgba(42, 171, 238, 0.3); + text-align: center; + + .login-gate-icon { + margin: 0 auto 10px; + width: 56px; + height: 56px; + border-radius: 50%; + background: rgba(42, 171, 238, 0.1); + color: #2AABEE; + display: flex; + align-items: center; + justify-content: center; + } + + .login-gate-title { + margin: 0 0 4px; + font-size: 1rem; + font-weight: 700; + color: #1a1a1a; + } + + .login-gate-desc { + margin: 0 0 16px; + font-size: 0.85rem; + color: #6b7280; + line-height: 1.4; + } + + .telegram-login-btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 12px 24px; + border: none; + border-radius: 10px; + background: #2AABEE; + color: #fff; + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: #229ED9; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(42, 171, 238, 0.3); + } + } + + .login-gate-qr { + margin-top: 14px; + + .qr-hint { + margin: 0 0 8px; + font-size: 0.8rem; + color: #999; + } + + .qr-wrapper { + display: inline-flex; + padding: 10px; + background: #fff; + border-radius: 10px; + border: 1px solid #e5e7eb; + + img { + display: block; + border-radius: 4px; + } + } + } + } } // Novo Cart Summary - Green Modern diff --git a/src/app/pages/cart/cart.component.ts b/src/app/pages/cart/cart.component.ts index e0e6065..aac63c3 100644 --- a/src/app/pages/cart/cart.component.ts +++ b/src/app/pages/cart/cart.component.ts @@ -7,15 +7,16 @@ 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 { TelegramLoginComponent } from '../../components/telegram-login/telegram-login.component'; import { environment } from '../../../environments/environment'; -import { getDiscountedPrice, getMainImage, trackByItemId, getBadgeClass } from '../../utils/item.utils'; +import { getDiscountedPrice, getMainImage, trackByItemId, 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-cart', - imports: [DecimalPipe, RouterLink, FormsModule, EmptyCartIconComponent, LangRoutePipe, TranslatePipe], + imports: [DecimalPipe, RouterLink, FormsModule, EmptyCartIconComponent, TelegramLoginComponent, LangRoutePipe, TranslatePipe], templateUrl: './cart.component.html', styleUrls: ['./cart.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush @@ -30,6 +31,9 @@ export class CartComponent implements OnDestroy { private i18n = inject(TranslateService); private authService = inject(AuthService); + isAuthenticated = this.authService.isAuthenticated; + loginUrl = signal(''); + // Swipe state swipedItemId = signal(null); @@ -64,6 +68,11 @@ export class CartComponent implements OnDestroy { this.items = this.cartService.items; this.itemCount = this.cartService.itemCount; this.totalPrice = this.cartService.totalPrice; + this.loginUrl.set(this.authService.getTelegramLoginUrl()); + } + + requestLogin(): void { + this.authService.requestLogin(); } ngOnDestroy(): void { @@ -131,6 +140,10 @@ export class CartComponent implements OnDestroy { readonly getDiscountedPrice = getDiscountedPrice; readonly getBadgeClass = getBadgeClass; + itemName(item: Item): string { return getTranslatedField(item, 'name', this.langService.currentLanguage()); } + itemDesc(item: Item): string { return getTranslatedField(item, 'simpleDescription', this.langService.currentLanguage()); } + get currentCurrency(): string { return this.langService.currentCurrency(); } + checkout(): void { if (!this.termsAccepted) { alert(this.i18n.t('cart.acceptTerms')); @@ -174,7 +187,7 @@ export class CartComponent implements OnDestroy { const paymentData = { amount: this.totalPrice(), - currency: 'RUB', + currency: this.langService.currentCurrency(), siteuserID: userId, siteorderID: orderId, redirectUrl: '', diff --git a/src/app/pages/category/category.component.html b/src/app/pages/category/category.component.html index 4c1e677..e925de1 100644 --- a/src/app/pages/category/category.component.html +++ b/src/app/pages/category/category.component.html @@ -12,7 +12,7 @@
- + @if (item.discount > 0) {
-{{ item.discount }}%
} @@ -26,7 +26,7 @@
-

{{ item.name }}

+

{{ itemName(item) }}

⭐ {{ item.rating }} diff --git a/src/app/pages/category/category.component.ts b/src/app/pages/category/category.component.ts index ec9a18c..5281624 100644 --- a/src/app/pages/category/category.component.ts +++ b/src/app/pages/category/category.component.ts @@ -1,10 +1,11 @@ -import { Component, OnInit, OnDestroy, signal, HostListener, ChangeDetectionStrategy } from '@angular/core'; +import { Component, OnInit, OnDestroy, signal, HostListener, ChangeDetectionStrategy, inject } from '@angular/core'; import { DecimalPipe } from '@angular/common'; import { ActivatedRoute, RouterLink } from '@angular/router'; import { ApiService, CartService } from '../../services'; import { Item } from '../../models'; import { Subscription } from 'rxjs'; -import { getDiscountedPrice, getMainImage, trackByItemId, getBadgeClass } from '../../utils/item.utils'; +import { getDiscountedPrice, getMainImage, trackByItemId, 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'; @@ -23,7 +24,7 @@ export class CategoryComponent implements OnInit, OnDestroy { hasMore = signal(true); private skip = 0; - private readonly count = 20; + private readonly count = 50; private isLoadingMore = false; private routeSubscription?: Subscription; private scrollTimeout?: ReturnType; @@ -108,4 +109,7 @@ export class CategoryComponent implements OnInit, OnDestroy { readonly getMainImage = getMainImage; readonly trackByItemId = trackByItemId; readonly getBadgeClass = getBadgeClass; + + private langService = inject(LanguageService); + itemName(item: Item): string { return getTranslatedField(item, 'name', this.langService.currentLanguage()); } } diff --git a/src/app/pages/category/subcategories.component.html b/src/app/pages/category/subcategories.component.html index c117e1f..d43dcd6 100644 --- a/src/app/pages/category/subcategories.component.html +++ b/src/app/pages/category/subcategories.component.html @@ -18,6 +18,30 @@

{{ parentName() }}

+ + @if (nestedSubcategories().length > 0) { +
+ } + + @if (subcategories().length > 0) {
@for (cat of subcategories(); track trackByCategoryId($index, cat)) { @@ -35,7 +59,53 @@ }
- } @else { + } + + + @if (categoryItems().length > 0) { + + } + + @if (!hasSubcategories() && categoryItems().length === 0) {
diff --git a/src/app/pages/category/subcategories.component.scss b/src/app/pages/category/subcategories.component.scss index f33a949..74fc4f0 100644 --- a/src/app/pages/category/subcategories.component.scss +++ b/src/app/pages/category/subcategories.component.scss @@ -235,6 +235,149 @@ min-height: calc(2 * 1.3em); } + .category-count { + font-family: "DM Sans", sans-serif; + font-size: 0.8rem; + color: #697777; + } + + // Items section within subcategories page + .category-items-section { + margin-top: 40px; + + .items-section-title { + font-family: "DM Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-size: 1.5rem; + font-weight: 700; + color: #1e3c38; + margin: 0 0 20px 0; + } + } + + .items-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 24px; + } + + .item-card { + display: flex; + flex-direction: column; + text-decoration: none; + border: 1px solid #d3dad9; + border-radius: 13px; + overflow: hidden; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + background: #fff; + + &:hover { + transform: translateY(-4px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); + } + } + + .item-image { + position: relative; + aspect-ratio: 1; + overflow: hidden; + background: #f5f5f5; + + img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; + } + + .item-card:hover & img { + transform: scale(1.05); + } + + .item-discount { + position: absolute; + top: 8px; + right: 8px; + background: #dc2626; + color: white; + font-size: 0.75rem; + font-weight: 700; + padding: 2px 8px; + border-radius: 6px; + } + + .item-badges { + position: absolute; + top: 8px; + left: 8px; + display: flex; + gap: 4px; + flex-wrap: wrap; + } + + .item-badge { + font-size: 0.65rem; + font-weight: 600; + padding: 2px 6px; + border-radius: 4px; + text-transform: uppercase; + } + } + + .item-info { + padding: 12px; + display: flex; + flex-direction: column; + gap: 8px; + } + + .item-name { + font-family: "DM Sans", sans-serif; + font-size: 0.9rem; + font-weight: 600; + color: #1e3c38; + margin: 0; + line-height: 1.3; + display: -webkit-box; + line-clamp: 2; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .item-price { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + + .old-price { + font-size: 0.8rem; + color: #a1b4b5; + text-decoration: line-through; + } + + .current-price { + font-size: 1rem; + font-weight: 700; + color: #1e3c38; + } + } + + .item-cart-btn { + align-self: flex-end; + background: #497671; + color: white; + border: none; + border-radius: 8px; + padding: 6px 10px; + cursor: pointer; + transition: background 0.2s ease; + + &:hover { + background: #3a5f5b; + } + } + // Keyframes @keyframes spin { to { transform: rotate(360deg); } @@ -248,6 +391,11 @@ grid-template-columns: repeat(3, 1fr); gap: 24px; } + + .items-grid { + grid-template-columns: repeat(3, 1fr); + gap: 20px; + } } @media (max-width: 992px) { @@ -273,6 +421,11 @@ gap: 16px; } + .items-grid { + grid-template-columns: repeat(2, 1fr); + gap: 16px; + } + .category-info { padding: 10px 12px; } @@ -294,6 +447,11 @@ gap: 12px; } + .items-grid { + grid-template-columns: repeat(2, 1fr); + gap: 12px; + } + .category-info { padding: 8px 10px; } diff --git a/src/app/pages/category/subcategories.component.ts b/src/app/pages/category/subcategories.component.ts index 310ee91..d709501 100644 --- a/src/app/pages/category/subcategories.component.ts +++ b/src/app/pages/category/subcategories.component.ts @@ -1,15 +1,17 @@ import { Component, OnInit, OnDestroy, signal, ChangeDetectionStrategy, inject } from '@angular/core'; +import { DecimalPipe } from '@angular/common'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; -import { ApiService, LanguageService } from '../../services'; -import { Category } from '../../models'; +import { ApiService, CartService, LanguageService } from '../../services'; +import { Category, Item, Subcategory } 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'; +import { getDiscountedPrice, getMainImage, trackByItemId, getBadgeClass, getTranslatedField } from '../../utils/item.utils'; @Component({ selector: 'app-subcategories', - imports: [RouterLink, LangRoutePipe, TranslatePipe], + imports: [DecimalPipe, RouterLink, LangRoutePipe, TranslatePipe], templateUrl: './subcategories.component.html', styleUrls: ['./subcategories.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush @@ -17,6 +19,10 @@ import { TranslateService } from '../../i18n/translate.service'; export class SubcategoriesComponent implements OnInit, OnDestroy { categories = signal([]); subcategories = signal([]); + /** Nested subcategories from API with hasItems support */ + nestedSubcategories = signal([]); + /** Items belonging directly to this category (when hasItems is true) */ + categoryItems = signal([]); loading = signal(true); error = signal(null); @@ -29,7 +35,8 @@ export class SubcategoriesComponent implements OnInit, OnDestroy { private route: ActivatedRoute, private router: Router, private apiService: ApiService, - private langService: LanguageService + private langService: LanguageService, + private cartService: CartService ) {} ngOnInit(): void { @@ -45,19 +52,40 @@ export class SubcategoriesComponent implements OnInit, OnDestroy { private loadForParent(parentID: number): void { this.loading.set(true); + this.categoryItems.set([]); + this.nestedSubcategories.set([]); + this.apiService.getCategories().subscribe({ next: (cats) => { 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.i18n.t('home.categoriesTitle')); - if (!subs || subs.length === 0) { + // Check for nested subcategories from API response (backOffice format) + const nested = parent?.subcategories || []; + const visibleNested = nested.filter(s => s.visible !== false); + + // Also check flat legacy subcategories + const flatSubs = cats.filter(c => c.parentID === parentID); + + if (visibleNested.length > 0) { + // Use nested subcategories from API + this.nestedSubcategories.set(visibleNested); + this.subcategories.set([]); + + // If this category itself has items, load them too + this.loadCategoryItems(parentID); + } else if (flatSubs.length > 0) { + // Legacy flat subcategories + this.subcategories.set(flatSubs); + this.nestedSubcategories.set([]); + + // Also load items for this category in case it has direct items + this.loadCategoryItems(parentID); + } else { // No subcategories: redirect to items list for this category const lang = this.langService.currentLanguage(); this.router.navigate([`/${lang}/category`, parentID, 'items'], { replaceUrl: true }); - } else { - this.subcategories.set(subs); } this.loading.set(false); @@ -70,8 +98,41 @@ export class SubcategoriesComponent implements OnInit, OnDestroy { }); } + /** Load items that belong directly to this category */ + private loadCategoryItems(categoryID: number): void { + this.apiService.getCategoryItems(categoryID, 50, 0).subscribe({ + next: (items) => { + this.categoryItems.set(items); + }, + error: () => { + // Not critical — subcategories still work + } + }); + } + + hasSubcategories(): boolean { + return this.subcategories().length > 0 || this.nestedSubcategories().length > 0; + } + + addToCart(itemID: number, event: Event): void { + event.preventDefault(); + event.stopPropagation(); + this.cartService.addItem(itemID); + } + // TrackBy function for performance optimization trackByCategoryId(index: number, category: Category): number { return category.categoryID; } + + trackBySubId(index: number, sub: Subcategory): string { + return sub.id; + } + + readonly getDiscountedPrice = getDiscountedPrice; + readonly getMainImage = getMainImage; + readonly trackByItemId = trackByItemId; + readonly getBadgeClass = getBadgeClass; + + itemName(item: Item): string { return getTranslatedField(item, 'name', this.langService.currentLanguage()); } } diff --git a/src/app/pages/info/faq/en/faq-en.component.html b/src/app/pages/info/faq/en/faq-en.component.html index 609c895..f733131 100644 --- a/src/app/pages/info/faq/en/faq-en.component.html +++ b/src/app/pages/info/faq/en/faq-en.component.html @@ -1,240 +1,244 @@ -

Frequently Asked Questions (FAQ) 📌

- - - - - - - - - - - - - - - - - - - - +
diff --git a/src/app/pages/info/faq/hy/faq-hy.component.html b/src/app/pages/info/faq/hy/faq-hy.component.html index 84a9258..f2a7986 100644 --- a/src/app/pages/info/faq/hy/faq-hy.component.html +++ b/src/app/pages/info/faq/hy/faq-hy.component.html @@ -1,240 +1,244 @@ -

Հաճախ տրվող հարցեր (FAQ) 📌

- - - - - - - - - - - - - - - - - - - - +
diff --git a/src/app/pages/info/faq/ru/faq-ru.component.html b/src/app/pages/info/faq/ru/faq-ru.component.html index 9972a3b..d4317c3 100644 --- a/src/app/pages/info/faq/ru/faq-ru.component.html +++ b/src/app/pages/info/faq/ru/faq-ru.component.html @@ -1,240 +1,244 @@ -

Часто задаваемые вопросы (FAQ) 📌

- - - - - - - - - - - - - - - - - - - - +
diff --git a/src/app/pages/item-detail/item-detail.component.html b/src/app/pages/item-detail/item-detail.component.html index b2508c1..513f82a 100644 --- a/src/app/pages/item-detail/item-detail.component.html +++ b/src/app/pages/item-detail/item-detail.component.html @@ -102,6 +102,34 @@ }
+ @if (item()!.colour || item()!.size) { +
+ @if (item()!.colour) { +
+ {{ 'itemDetail.colour' | translate }}: + {{ item()!.colour }} +
+ } + @if (item()!.size) { +
+ {{ 'itemDetail.size' | translate }}: + {{ item()!.size }} +
+ } +
+ } + + @if (item()!.attributes && item()!.attributes!.length > 0) { +
+ @for (attr of item()!.attributes!; track attr.key) { +
+ {{ attr.key }} + {{ attr.value }} +
+ } +
+ } +
+ @if (item()!.colour || item()!.size) { +
+ @if (item()!.colour) { +
+ {{ 'itemDetail.colour' | translate }}: + {{ item()!.colour }} +
+ } + @if (item()!.size) { +
+ {{ 'itemDetail.size' | translate }}: + {{ item()!.size }} +
+ } +
+ } + + @if (item()!.attributes && item()!.attributes!.length > 0) { +
+ @for (attr of item()!.attributes!; track attr.key) { +
+ {{ attr.key }} + {{ attr.value }} +
+ } +
+ } +
diff --git a/src/app/pages/legal/company-details/hy/company-details-hy.component.html b/src/app/pages/legal/company-details/hy/company-details-hy.component.html index 418a376..d5ec9d8 100644 --- a/src/app/pages/legal/company-details/hy/company-details-hy.component.html +++ b/src/app/pages/legal/company-details/hy/company-details-hy.component.html @@ -1,98 +1,102 @@ -

Կազմակերպության տվյալներ

- - - - - - - - - - - - - - +
diff --git a/src/app/pages/legal/company-details/ru/company-details-ru.component.html b/src/app/pages/legal/company-details/ru/company-details-ru.component.html index ab010c2..217f9e6 100644 --- a/src/app/pages/legal/company-details/ru/company-details-ru.component.html +++ b/src/app/pages/legal/company-details/ru/company-details-ru.component.html @@ -1,98 +1,102 @@ -

Реквизиты организации

- - - - - - - - - - - - - - +
diff --git a/src/app/pages/legal/payment-terms/en/payment-terms-en.component.html b/src/app/pages/legal/payment-terms/en/payment-terms-en.component.html index 9208c0d..f0cdbff 100644 --- a/src/app/pages/legal/payment-terms/en/payment-terms-en.component.html +++ b/src/app/pages/legal/payment-terms/en/payment-terms-en.component.html @@ -1,113 +1,117 @@ -

Payment Terms

- - - - - - - - - - - - - - - - + diff --git a/src/app/pages/legal/payment-terms/hy/payment-terms-hy.component.html b/src/app/pages/legal/payment-terms/hy/payment-terms-hy.component.html index a463978..3ff9627 100644 --- a/src/app/pages/legal/payment-terms/hy/payment-terms-hy.component.html +++ b/src/app/pages/legal/payment-terms/hy/payment-terms-hy.component.html @@ -1,113 +1,117 @@ -

Վճարման կանոններ

- - - - - - - - - - - - - - - - + diff --git a/src/app/pages/legal/payment-terms/ru/payment-terms-ru.component.html b/src/app/pages/legal/payment-terms/ru/payment-terms-ru.component.html index 4a3f548..56117e3 100644 --- a/src/app/pages/legal/payment-terms/ru/payment-terms-ru.component.html +++ b/src/app/pages/legal/payment-terms/ru/payment-terms-ru.component.html @@ -1,113 +1,117 @@ -

Правила оплаты

- - - - - - - - - - - - - - - - + diff --git a/src/app/pages/legal/privacy-policy/en/privacy-policy-en.component.html b/src/app/pages/legal/privacy-policy/en/privacy-policy-en.component.html index 06c6c81..82d1260 100644 --- a/src/app/pages/legal/privacy-policy/en/privacy-policy-en.component.html +++ b/src/app/pages/legal/privacy-policy/en/privacy-policy-en.component.html @@ -1,3 +1,5 @@ + diff --git a/src/app/pages/legal/privacy-policy/hy/privacy-policy-hy.component.html b/src/app/pages/legal/privacy-policy/hy/privacy-policy-hy.component.html index 06c6c81..82d1260 100644 --- a/src/app/pages/legal/privacy-policy/hy/privacy-policy-hy.component.html +++ b/src/app/pages/legal/privacy-policy/hy/privacy-policy-hy.component.html @@ -1,3 +1,5 @@ + diff --git a/src/app/pages/legal/privacy-policy/ru/privacy-policy-ru.component.html b/src/app/pages/legal/privacy-policy/ru/privacy-policy-ru.component.html index 5edbdd1..ca811db 100644 --- a/src/app/pages/legal/privacy-policy/ru/privacy-policy-ru.component.html +++ b/src/app/pages/legal/privacy-policy/ru/privacy-policy-ru.component.html @@ -1,3 +1,5 @@ + diff --git a/src/app/pages/legal/public-offer/en/public-offer-en.component.html b/src/app/pages/legal/public-offer/en/public-offer-en.component.html index d9d79fc..95753d8 100644 --- a/src/app/pages/legal/public-offer/en/public-offer-en.component.html +++ b/src/app/pages/legal/public-offer/en/public-offer-en.component.html @@ -1,3 +1,5 @@ + diff --git a/src/app/pages/legal/public-offer/hy/public-offer-hy.component.html b/src/app/pages/legal/public-offer/hy/public-offer-hy.component.html index 28d90c0..644439a 100644 --- a/src/app/pages/legal/public-offer/hy/public-offer-hy.component.html +++ b/src/app/pages/legal/public-offer/hy/public-offer-hy.component.html @@ -1 +1,5 @@ -

Հdelays DELAYS ՀԱՄDELAYS

+ diff --git a/src/app/pages/legal/public-offer/ru/public-offer-ru.component.html b/src/app/pages/legal/public-offer/ru/public-offer-ru.component.html index 7292a54..47a120f 100644 --- a/src/app/pages/legal/public-offer/ru/public-offer-ru.component.html +++ b/src/app/pages/legal/public-offer/ru/public-offer-ru.component.html @@ -1,3 +1,5 @@ + diff --git a/src/app/pages/legal/return-policy/en/return-policy-en.component.html b/src/app/pages/legal/return-policy/en/return-policy-en.component.html index 6943f39..025b6a2 100644 --- a/src/app/pages/legal/return-policy/en/return-policy-en.component.html +++ b/src/app/pages/legal/return-policy/en/return-policy-en.component.html @@ -1,3 +1,5 @@ + diff --git a/src/app/pages/legal/return-policy/hy/return-policy-hy.component.html b/src/app/pages/legal/return-policy/hy/return-policy-hy.component.html index b0125cd..cf47640 100644 --- a/src/app/pages/legal/return-policy/hy/return-policy-hy.component.html +++ b/src/app/pages/legal/return-policy/hy/return-policy-hy.component.html @@ -1,3 +1,5 @@ + diff --git a/src/app/pages/legal/return-policy/ru/return-policy-ru.component.html b/src/app/pages/legal/return-policy/ru/return-policy-ru.component.html index be676f1..7863b7c 100644 --- a/src/app/pages/legal/return-policy/ru/return-policy-ru.component.html +++ b/src/app/pages/legal/return-policy/ru/return-policy-ru.component.html @@ -1,3 +1,5 @@ + diff --git a/src/app/pages/search/search.component.html b/src/app/pages/search/search.component.html index a1de3fa..37c7b00 100644 --- a/src/app/pages/search/search.component.html +++ b/src/app/pages/search/search.component.html @@ -59,7 +59,7 @@
- + @if (item.discount > 0) {
-{{ item.discount }}%
} @@ -73,10 +73,10 @@
-

{{ item.name }}

+

{{ itemName(item) }}

- @if (item.simpleDescription) { -

{{ item.simpleDescription }}

+ @if (itemDesc(item)) { +

{{ itemDesc(item) }}

}
diff --git a/src/app/pages/search/search.component.ts b/src/app/pages/search/search.component.ts index c7af37e..be8acc4 100644 --- a/src/app/pages/search/search.component.ts +++ b/src/app/pages/search/search.component.ts @@ -6,7 +6,8 @@ import { ApiService, CartService } from '../../services'; import { Item } from '../../models'; import { Subject, Subscription } from 'rxjs'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; -import { getDiscountedPrice, getMainImage, trackByItemId, getBadgeClass } from '../../utils/item.utils'; +import { getDiscountedPrice, getMainImage, trackByItemId, 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'; import { TranslateService } from '../../i18n/translate.service'; @@ -27,7 +28,7 @@ export class SearchComponent implements OnDestroy { totalResults = signal(0); private skip = 0; - private readonly count = 20; + private readonly count = 50; private isLoadingMore = false; private searchSubject = new Subject(); private searchSubscription: Subscription; @@ -137,4 +138,8 @@ export class SearchComponent implements OnDestroy { readonly getMainImage = getMainImage; readonly trackByItemId = trackByItemId; readonly getBadgeClass = getBadgeClass; + + private langService = inject(LanguageService); + itemName(item: Item): string { return getTranslatedField(item, 'name', this.langService.currentLanguage()); } + itemDesc(item: Item): string { return getTranslatedField(item, 'simpleDescription', this.langService.currentLanguage()); } } diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts index a72c2a1..2b46f85 100644 --- a/src/app/services/api.service.ts +++ b/src/app/services/api.service.ts @@ -18,7 +18,8 @@ export class ApiService { * legacy marketplace format and the new backOffice API format. */ private normalizeItem(raw: any): Item { - const item: Item = { ...raw }; + const { partnerID, ...rest } = raw; + const item: Item = { ...rest }; // Map backOffice string id → legacy numeric itemID if (raw.id != null && raw.itemID == null) { @@ -30,6 +31,13 @@ export class ApiService { if (raw.imgs && (!raw.photos || raw.photos.length === 0)) { item.photos = raw.imgs.map((url: string) => ({ url })); } + // Normalize photo type: API sends type='video'|'photo', template checks .video + if (item.photos) { + item.photos = item.photos.map((p: any) => ({ + ...p, + video: p.video || (p.type === 'video' ? p.url : undefined), + })); + } item.imgs = raw.imgs || raw.photos?.map((p: any) => p.url) || []; // Map backOffice description (key-value array) → legacy description string @@ -40,6 +48,33 @@ export class ApiService { item.description = raw.description || raw.simpleDescription || ''; } + // Map backend names[] → translations (multi-lang name support) + if (raw.names && Array.isArray(raw.names)) { + item.names = raw.names; + if (!item.translations) item.translations = {}; + for (const entry of raw.names) { + if (!item.translations[entry.language]) item.translations[entry.language] = {}; + item.translations[entry.language].name = entry.value; + } + } + + // Map backend descriptions[] → translations (multi-lang descriptions) + if (raw.descriptions && Array.isArray(raw.descriptions)) { + item.descriptions = raw.descriptions; + if (!item.translations) item.translations = {}; + for (const entry of raw.descriptions) { + if (!item.translations[entry.language]) item.translations[entry.language] = {}; + item.translations[entry.language].simpleDescription = entry.value; + } + } + + // Preserve attributes from backend + item.attributes = raw.attributes || []; + + // Preserve colour & size + item.colour = raw.colour || ''; + item.size = raw.size || ''; + // Map backOffice comments → legacy callbacks if (raw.comments && (!raw.callbacks || raw.callbacks.length === 0)) { item.callbacks = raw.comments.map((c: any) => ({ @@ -77,7 +112,7 @@ export class ApiService { item.badges = raw.badges || []; item.tags = raw.tags || []; item.simpleDescription = raw.simpleDescription || ''; - item.translations = raw.translations || {}; + item.translations = item.translations || raw.translations || {}; item.visible = raw.visible ?? true; item.priority = raw.priority ?? 0; diff --git a/src/app/services/language.service.ts b/src/app/services/language.service.ts index 12232db..612327f 100644 --- a/src/app/services/language.service.ts +++ b/src/app/services/language.service.ts @@ -9,11 +9,18 @@ export interface Language { enabled: boolean; } +export interface Currency { + code: string; + symbol: string; + name: string; +} + @Injectable({ providedIn: 'root' }) export class LanguageService { private currentLanguageSignal = signal('ru'); + private currentCurrencySignal = signal('RUB'); languages: Language[] = [ { code: 'ru', name: 'Русский', flag: '🇷🇺', flagSvg: '/flags/ru.svg', enabled: true }, @@ -21,7 +28,15 @@ export class LanguageService { { code: 'hy', name: 'Հայերեն', flag: '🇦🇲', flagSvg: '/flags/arm.svg', enabled: true } ]; + currencies: Currency[] = [ + { code: 'RUB', symbol: '₽', name: 'Рубль' }, + { code: 'USD', symbol: '$', name: 'Dollar' }, + { code: 'EUR', symbol: '€', name: 'Euro' }, + { code: 'AMD', symbol: '֏', name: 'Դրամ' }, + ]; + currentLanguage = this.currentLanguageSignal.asReadonly(); + currentCurrency = this.currentCurrencySignal.asReadonly(); constructor(private router: Router) { // Load saved language from localStorage @@ -29,6 +44,11 @@ export class LanguageService { if (savedLang && this.languages.find(l => l.code === savedLang && l.enabled)) { this.currentLanguageSignal.set(savedLang); } + + const savedCurrency = localStorage.getItem('selectedCurrency'); + if (savedCurrency && this.currencies.find(c => c.code === savedCurrency)) { + this.currentCurrencySignal.set(savedCurrency); + } } setLanguage(langCode: string): void { @@ -39,6 +59,18 @@ export class LanguageService { } } + setCurrency(code: string): void { + const currency = this.currencies.find(c => c.code === code); + if (currency) { + this.currentCurrencySignal.set(code); + localStorage.setItem('selectedCurrency', code); + } + } + + getCurrentCurrency(): Currency | undefined { + return this.currencies.find(c => c.code === this.currentCurrencySignal()); + } + /** Change language and navigate to the same page with the new prefix */ switchLanguage(langCode: string): void { const lang = this.languages.find(l => l.code === langCode); diff --git a/src/app/utils/item.utils.ts b/src/app/utils/item.utils.ts index 13b2ebe..3d0eb16 100644 --- a/src/app/utils/item.utils.ts +++ b/src/app/utils/item.utils.ts @@ -61,17 +61,31 @@ export function getBadgeClass(badge: string): string { /** * Get the translated name/description for the current language. - * Falls back to the default (base) field if no translation exists. + * Checks translations map first, then names[]/descriptions[] arrays, + * then falls back to the default (base) field. */ export function getTranslatedField( item: Item, field: 'name' | 'simpleDescription', lang: string ): string { + // 1. Check translations map (backOffice format) const translation = item.translations?.[lang]; if (translation && translation[field]) { return translation[field]!; } + + // 2. Check names[]/descriptions[] arrays (backend API format) + if (field === 'name' && item.names?.length) { + const entry = item.names.find(n => n.language === lang); + if (entry) return entry.value; + } + if (field === 'simpleDescription' && item.descriptions?.length) { + const entry = item.descriptions.find(d => d.language === lang); + if (entry) return entry.value; + } + + // 3. Fallback to base field if (field === 'name') return item.name; if (field === 'simpleDescription') return item.simpleDescription || item.description || ''; return ''; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index f256912..a512e31 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,11 +1,11 @@ // Dexar Market Configuration export const environment = { production: false, - useMockData: true, // Toggle to test with backOffice mock data + useMockData: false, // Toggle to test with backOffice mock data brandName: 'Dexarmarket', brandFullName: 'Dexar Market', theme: 'dexar', - apiUrl: 'https://api.dexarmarket.ru:445', + apiUrl: '/api', logo: '/assets/images/dexar-logo.svg', contactEmail: 'info@dexarmarket.ru', supportEmail: 'info@dexarmarket.ru',