From 59bda137e5e6144981c89e2f044271f345447b59 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Tue, 5 May 2026 00:52:03 +0400 Subject: [PATCH] changes --- dist/index.html | 2 +- public/alipay.svg | 5 + public/flags/arm.svg | 1 + public/flags/en.svg | 1 + public/flags/ru.svg | 1 + public/i18n/en.json | 57 ++++- public/i18n/hy.json | 57 ++++- public/i18n/ru.json | 57 ++++- src/app/api.ts | 3 + src/app/app.routes.ts | 15 ++ src/app/pages/about-page/about-page.html | 40 ++++ src/app/pages/about-page/about-page.scss | 75 ++++++ src/app/pages/about-page/about-page.ts | 10 + .../pages/contacts-page/contacts-page.html | 66 ++++++ .../pages/contacts-page/contacts-page.scss | 146 ++++++++++++ src/app/pages/contacts-page/contacts-page.ts | 10 + src/app/pages/create-page/create-page.html | 20 +- src/app/pages/create-page/create-page.scss | 33 +++ src/app/pages/create-page/create-page.ts | 185 ++++++++++++--- .../pages/fastcheck-page/fastcheck-page.html | 29 ++- .../pages/fastcheck-page/fastcheck-page.scss | 34 +++ .../pages/fastcheck-page/fastcheck-page.ts | 84 +++++-- .../pages/legacy-pay-page/legacy-pay-page.ts | 12 +- .../pages/partners-page/partners-page.html | 26 +++ .../pages/partners-page/partners-page.scss | 146 ++++++++++++ src/app/pages/partners-page/partners-page.ts | 26 +++ src/app/site-header/site-header.html | 76 +++++-- src/app/site-header/site-header.scss | 215 +++++++++++++++--- src/app/site-header/site-header.ts | 33 ++- 29 files changed, 1347 insertions(+), 118 deletions(-) create mode 100644 public/alipay.svg create mode 100644 public/flags/arm.svg create mode 100644 public/flags/en.svg create mode 100644 public/flags/ru.svg create mode 100644 src/app/pages/about-page/about-page.html create mode 100644 src/app/pages/about-page/about-page.scss create mode 100644 src/app/pages/about-page/about-page.ts create mode 100644 src/app/pages/contacts-page/contacts-page.html create mode 100644 src/app/pages/contacts-page/contacts-page.scss create mode 100644 src/app/pages/contacts-page/contacts-page.ts create mode 100644 src/app/pages/partners-page/partners-page.html create mode 100644 src/app/pages/partners-page/partners-page.scss create mode 100644 src/app/pages/partners-page/partners-page.ts diff --git a/dist/index.html b/dist/index.html index a357bf6..24b7696 100644 --- a/dist/index.html +++ b/dist/index.html @@ -14,5 +14,5 @@ - + diff --git a/public/alipay.svg b/public/alipay.svg new file mode 100644 index 0000000..cd9e6d7 --- /dev/null +++ b/public/alipay.svg @@ -0,0 +1,5 @@ + + + A + diff --git a/public/flags/arm.svg b/public/flags/arm.svg new file mode 100644 index 0000000..b83d158 --- /dev/null +++ b/public/flags/arm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/en.svg b/public/flags/en.svg new file mode 100644 index 0000000..b3d4b3b --- /dev/null +++ b/public/flags/en.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/flags/ru.svg b/public/flags/ru.svg new file mode 100644 index 0000000..6a66ca4 --- /dev/null +++ b/public/flags/ru.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/i18n/en.json b/public/i18n/en.json index 206b1fd..b7dcda2 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -2,10 +2,12 @@ "header": { "nav_about": "About", "nav_contacts": "Contacts", + "nav_partners": "Partners", "nav_support": "Support", "aria_nav": "Navigation", "aria_menu": "Mobile menu", - "aria_burger": "Menu" + "aria_burger": "Menu", + "aria_close": "Close menu" }, "footer": { "desc": "An innovative virtual check service for individuals. Create digital checks online and cash them out at partner bank ATMs 24/7.", @@ -55,7 +57,8 @@ "note_label": "Note", "note_placeholder": "Reason for payment...", "creating": "Creating…", - "create_btn": "Create" + "create_btn": "Create", + "amount_hint": "Allowed amount:" }, "sbp": { "title": "Pay via SBP", @@ -67,7 +70,57 @@ "pay_loading": "Please wait...", "pay_btn": "Proceed to payment" }, + "about": { + "title": "About the service", + "lead": "fastCHECK is an innovative virtual check service for individuals, available 24/7.", + "what_title": "What is fastCHECK?", + "what_text": "fastCHECK is a digital check you create online and cash out at partner bank ATMs at any time of day. No queues, no offices — just your phone and the nearest ATM.", + "how_title": "How does it work?", + "step1": "Log in and create a new fastCHECK with the required amount.", + "step2": "Save the check number and 5-digit code.", + "step3": "Enter the details on the site and confirm via Telegram.", + "step4": "Receive the funds in a convenient way.", + "why_title": "Why fastCHECK?", + "why1": "Available 24/7 — including weekends and holidays.", + "why2": "Secure authorisation via Telegram.", + "why3": "Supports SBP and other popular payment methods.", + "why4": "Fast processing — from seconds to a few minutes.", + "company_title": "About the company", + "company_text": "The service is developed by LLC VIAEXPORT (TIN 9909675800). The company is registered in Russia and Armenia. Legal address: Armenia, 0201, Yerevan, Minskaya St. 21-23, apt. 44." + }, + "contacts": { + "title": "Contacts", + "lead": "We are available 24/7. Choose your preferred way to reach us.", + "ru_label": "Phone — Russia", + "am_label": "Phone — Armenia", + "email_label": "Email", + "tg_label": "Telegram bot", + "hours_title": "Working hours" + }, + "errors": { + "not_found": "Payment not found or expired.", + "lookup_failed": "Could not verify the number. Please try again.", + "session_failed": "Could not create a session. Please try again.", + "payment_failed": "Could not process the payment. Check the code and try again.", + "invalid_code": "Invalid code. Please check and try again.", + "invalid_amount": "Please enter a valid amount." + }, "common": { "secure": "Secure connection" + }, + "partners": { + "title": "Partners", + "lead": "Stores, services and companies accepting fastCHECK as a payment method.", + "cat_finance": "Finance", + "cat_retail": "Retail", + "cat_hotels": "Hotels", + "cat_services": "Services", + "p1_desc": "Currency exchange and transfers across Armenia.", + "p2_desc": "Forex broker supporting fastCHECK for account top-ups.", + "p3_desc": "Online retailer with delivery across Russia and CIS.", + "p4_desc": "Hotel booking and payment via fastCHECK.", + "cta_title": "Want to become a partner?", + "cta_text": "Connect fastCHECK to your business — fast, with minimal paperwork.", + "cta_btn": "Contact us" } } diff --git a/public/i18n/hy.json b/public/i18n/hy.json index e36bca5..d59ae92 100644 --- a/public/i18n/hy.json +++ b/public/i18n/hy.json @@ -2,10 +2,12 @@ "header": { "nav_about": "Ծառայության մասին", "nav_contacts": "Կապ", + "nav_partners": "Գործընկերներ", "nav_support": "Աջակցություն", "aria_nav": "Նավիգացիա", "aria_menu": "Բջջային ընտրացանկ", - "aria_burger": "Ընտրացանկ" + "aria_burger": "Ընտրացանկ", + "aria_close": "Փակել ընտրացանկը" }, "footer": { "desc": "Ֆիզիկական անձանց համար վիրտուալ չեկերի նորարարական ծառայություն: Ստեղծեք թվային չեկեր առցանց և կանխիկացրեք դրանք գործընկեր բանկերի բանկոմատներում 24/7:", @@ -55,7 +57,8 @@ "note_label": "Նշում", "note_placeholder": "Վճարման պատճառ...", "creating": "Ստեղծվում է…", - "create_btn": "Ստեղծել" + "create_btn": "Ստեղծել", + "amount_hint": "Թույլատրելի գումար՝" }, "sbp": { "title": "Վճարել SBP-ով", @@ -67,7 +70,57 @@ "pay_loading": "Սպասեք...", "pay_btn": "Անցնել վճարմանը" }, + "about": { + "title": "Ծառայության մասին", + "lead": "fastCHECK-ը ֆիզիկական անձանց համար վիրտուալ չեկերի նորարարական ծառայություն է, հասանելի 24/7:", + "what_title": "Ի՞նչ է fastCHECK-ը", + "what_text": "fastCHECK-ը թվային չեկ է, որը ստեղծում եք առցանց և կանխիկացնում գործընկեր բանկերի բանկոմատներում: Հերթեր չկան, գրասենյակներ չկան — միայն հեռախոս և ամենամոտ բանկոմատ:", + "how_title": "Ինչպե՞ս է դա աշխատում", + "step1": "Մուտք գործեք և ստեղծեք նոր fastCHECK անհրաժեշտ գումարով:", + "step2": "Պահպանեք չեկի համարն ու 5-նիշ կոդը:", + "step3": "Մուտքագրեք տվյալները կայքում և հաստատեք Telegram-ի միջոցով:", + "step4": "Ստացեք գումարն ձեզ հարմար ձևով:", + "why_title": "Ինչու՞ fastCHECK", + "why1": "Հասանելի 24/7 — ներառյալ հանգստյան և տոն օրերը:", + "why2": "Անվտանգ թույլտվություն Telegram-ի միջոցով:", + "why3": "Աջակցում է ՍԲՊ-ին և այլ հայտնի վճարման եղանակներ:", + "why4": "Արագ մշակում — վայրկյաններից մինչև մի քանի րոպե:", + "company_title": "Ընկերության մասին", + "company_text": "Ծառայությունը մշակվել է ООО «ВИАЭКСПОРТ»-ի կողմից (ИНН 9909675800): Ընկերությունը գրանցված է Ռուսաստանում և Հայաստանում: Իրավաբանական հասցե՝ Հայաստան, 0201, Երևան, Մինսկայա փ. 21-23, բն. 44:" + }, + "contacts": { + "title": "Կապ", + "lead": "Մենք կապի մեջ ենք 24/7: Ընտրեք կապի հարմար եղանակ:", + "ru_label": "Հեռախոս — Ռուսաստան", + "am_label": "Հեռախոս — Հայաստան", + "email_label": "Էլ. փոստ", + "tg_label": "Telegram-բոտ", + "hours_title": "Աշխատանքային ժամեր" + }, + "errors": { + "not_found": "Վճարումը չի գտնվել կամ ժամկետն անցել է:", + "lookup_failed": "Չհաջողվեց ստուգել համարը: Կրկին փորձեք:", + "session_failed": "Չհաջողվեց ստեղծել նիստ: Կրկին փորձեք:", + "payment_failed": "Չհաջողվեց մշակել վճարումը: Ստուգեք կոդը և կրկին փորձեք:", + "invalid_code": "Սխալ կոդ: Ստուգեք և կրկին մուտքագրեք:", + "invalid_amount": "Մուտքագրեք ճիշտ գումար:" + }, "common": { "secure": "Անվտանգ կապ" + }, + "partners": { + "title": "Գործընկերներ", + "lead": "Խանութներ, ծառայություններ և ընկերություններ, որոնք ընդունում են fastCHECK-ը:", + "cat_finance": "Ֆինանսներ", + "cat_retail": "Ռիթեյլ", + "cat_hotels": "Հյուրանոցներ", + "cat_services": "Ծառայություններ", + "p1_desc": "Արժույթի փոխանակում և փոխանցումներ ամբողջ Հայաստանում:", + "p2_desc": "Ֆորեքս բրոքեր fastCHECK-ով հաշիվ համալրման համար:", + "p3_desc": "Առցանց ռիթեյլ՝ Ռուսաստանով և ԱՊՀ-ով առաքմամբ:", + "p4_desc": "Հյուրանոցի ամրագրում և վճարում fastCHECK-ի միջոցով:", + "cta_title": "Ցանկանու՞մ եք դառնալ գործընկեր", + "cta_text": "Միացրեք fastCHECK-ը ձեր բիզնեսին — արագ, նվազ փաստաթղթերով:", + "cta_btn": "Կապվեք մեզ հետ" } } diff --git a/public/i18n/ru.json b/public/i18n/ru.json index e9825ab..67b7781 100644 --- a/public/i18n/ru.json +++ b/public/i18n/ru.json @@ -2,10 +2,12 @@ "header": { "nav_about": "О сервисе", "nav_contacts": "Контакты", + "nav_partners": "Партнёры", "nav_support": "Поддержка", "aria_nav": "Навигация", "aria_menu": "Мобильное меню", - "aria_burger": "Меню" + "aria_burger": "Меню", + "aria_close": "Закрыть меню" }, "footer": { "desc": "Инновационный сервис виртуальных чеков для физических лиц. Создавайте цифровые чеки онлайн и обналичивайте их через банкоматы банков-партнёров 24/7.", @@ -55,7 +57,8 @@ "note_label": "Примечание", "note_placeholder": "Причина платежа...", "creating": "Создание…", - "create_btn": "Создать" + "create_btn": "Создать", + "amount_hint": "Допустимая сумма:" }, "sbp": { "title": "Оплата через СБП", @@ -67,7 +70,57 @@ "pay_loading": "Подождите...", "pay_btn": "Перейти к оплате" }, + "about": { + "title": "О сервисе", + "lead": "fastCHECK — инновационный сервис виртуальных чеков для физических лиц, доступный 24/7.", + "what_title": "Что такое fastCHECK?", + "what_text": "fastCHECK — это цифровой чек, который вы создаёте онлайн и обналичиваете через банкоматы банков-партнёров в любое время суток. Никакой очереди, никаких офисов — только телефон и ближайший банкомат.", + "how_title": "Как это работает?", + "step1": "Зайдите в личный кабинет и создайте новый fastCHECK с нужной суммой.", + "step2": "Запомните или сохраните номер чека и 5-значный код.", + "step3": "Введите данные на сайте и подтвердите операцию через Telegram.", + "step4": "Получите средства удобным вам способом.", + "why_title": "Почему fastCHECK?", + "why1": "Работает 24/7 — включая выходные и праздники.", + "why2": "Безопасная авторизация через Telegram.", + "why3": "Поддержка СБП и других популярных методов оплаты.", + "why4": "Быстрое обслуживание — от секунд до нескольких минут.", + "company_title": "О компании", + "company_text": "Сервис разработан ООО «ВИАЭКСПОРТ» (ИНН 9909675800). Компания зарегистрирована в России и Армении, юридический адрес: Армения, 0201, Ереван, ул. Минская, дом 21-23, кв. 44." + }, + "contacts": { + "title": "Контакты", + "lead": "Мы на связи 24/7. Выберите удобный способ связи.", + "ru_label": "Телефон — Россия", + "am_label": "Телефон — Армения", + "email_label": "Электронная почта", + "tg_label": "Telegram-бот", + "hours_title": "Часы работы" + }, + "errors": { + "not_found": "Платёж не найден или просрочен.", + "lookup_failed": "Не удалось проверить номер. Попробуйте ещё раз.", + "session_failed": "Не удалось создать сессию. Попробуйте ещё раз.", + "payment_failed": "Не удалось принять платёж. Проверьте код и попробуйте снова.", + "invalid_code": "Неверный код. Проверьте и введите снова.", + "invalid_amount": "Введите корректную сумму." + }, "common": { "secure": "Защищённое соединение" + }, + "partners": { + "title": "Партнёры", + "lead": "Магазины, сервисы и компании, принимающие fastCHECK как способ оплаты.", + "cat_finance": "Финансы", + "cat_retail": "Ритейл", + "cat_hotels": "Отели", + "cat_services": "Услуги", + "p1_desc": "Обмен валют и переводы по всей Армении.", + "p2_desc": "Форекс-брокер с поддержкой fastCHECK для пополнения счёта.", + "p3_desc": "Онлайн-ритейлер с доставкой по России и СНГ.", + "p4_desc": "Бронирование и оплата проживания через fastCHECK.", + "cta_title": "Хотите стать партнёром?", + "cta_text": "Подключите fastCHECK к своему бизнесу — быстро, без лишних документов.", + "cta_btn": "Связаться с нами" } } diff --git a/src/app/api.ts b/src/app/api.ts index 70230fb..5e75fbb 100644 --- a/src/app/api.ts +++ b/src/app/api.ts @@ -6,3 +6,6 @@ export const FASTCHECK_API = 'https://api.fastcheck.store'; // Legacy QR endpoint kept for the SBP amount → payload redirect flow. export const QR_API = 'https://qr.vitanova.network:567/qr'; + +// New QR Vitanova API (dynamic QR, settings, polling). +export const QR_VITANOVA_API = 'https://qr.vitanova.network/api'; diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index dbccfc8..8ac1161 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -17,5 +17,20 @@ export const routes: Routes = [ loadComponent: () => import('./pages/create-page/create-page').then((m) => m.CreatePage) }, + { + path: 'about', + loadComponent: () => + import('./pages/about-page/about-page').then((m) => m.AboutPage) + }, + { + path: 'contacts', + loadComponent: () => + import('./pages/contacts-page/contacts-page').then((m) => m.ContactsPage) + }, + { + path: 'partners', + loadComponent: () => + import('./pages/partners-page/partners-page').then((m) => m.PartnersPage) + }, { path: '**', redirectTo: '' } ]; diff --git a/src/app/pages/about-page/about-page.html b/src/app/pages/about-page/about-page.html new file mode 100644 index 0000000..4773b01 --- /dev/null +++ b/src/app/pages/about-page/about-page.html @@ -0,0 +1,40 @@ +
+
+

{{ 'about.title' | translate }}

+

{{ 'about.lead' | translate }}

+
+ +
+ +
+

{{ 'about.what_title' | translate }}

+

{{ 'about.what_text' | translate }}

+
+ +
+

{{ 'about.how_title' | translate }}

+
    +
  1. {{ 'about.step1' | translate }}
  2. +
  3. {{ 'about.step2' | translate }}
  4. +
  5. {{ 'about.step3' | translate }}
  6. +
  7. {{ 'about.step4' | translate }}
  8. +
+
+ +
+

{{ 'about.why_title' | translate }}

+
    +
  • {{ 'about.why1' | translate }}
  • +
  • {{ 'about.why2' | translate }}
  • +
  • {{ 'about.why3' | translate }}
  • +
  • {{ 'about.why4' | translate }}
  • +
+
+ +
+

{{ 'about.company_title' | translate }}

+

{{ 'about.company_text' | translate }}

+
+ +
+
diff --git a/src/app/pages/about-page/about-page.scss b/src/app/pages/about-page/about-page.scss new file mode 100644 index 0000000..b987bbb --- /dev/null +++ b/src/app/pages/about-page/about-page.scss @@ -0,0 +1,75 @@ +:host { + display: block; + background: #f8fafc; + min-height: 100vh; +} + +// Shared info page layout — used by AboutPage and ContactsPage +.info-page { + max-width: 760px; + margin: 0 auto; + padding: 48px 24px 72px; + + @media (max-width: 600px) { + padding: 32px 16px 56px; + } + + &__hero { + margin-bottom: 48px; + border-bottom: 1px solid #e2e8f0; + padding-bottom: 32px; + } + + &__title { + font-size: 32px; + font-weight: 800; + color: #0f172a; + margin: 0 0 12px; + letter-spacing: -0.5px; + + @media (max-width: 600px) { font-size: 26px; } + } + + &__lead { + font-size: 17px; + line-height: 1.7; + color: #475569; + margin: 0; + } + + &__body { + display: flex; + flex-direction: column; + gap: 48px; + } +} + +.info-section { + &__title { + font-size: 20px; + font-weight: 700; + color: #1e293b; + margin: 0 0 14px; + } + + &__text { + font-size: 15.5px; + line-height: 1.75; + color: #475569; + margin: 0; + } + + &__steps, &__list { + padding-left: 22px; + margin: 0; + display: flex; + flex-direction: column; + gap: 10px; + + li { + font-size: 15.5px; + line-height: 1.65; + color: #475569; + } + } +} diff --git a/src/app/pages/about-page/about-page.ts b/src/app/pages/about-page/about-page.ts new file mode 100644 index 0000000..3979fea --- /dev/null +++ b/src/app/pages/about-page/about-page.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { TranslatePipe } from '../../translate/translate.pipe'; + +@Component({ + selector: 'app-about-page', + imports: [TranslatePipe], + templateUrl: './about-page.html', + styleUrl: './about-page.scss' +}) +export class AboutPage {} diff --git a/src/app/pages/contacts-page/contacts-page.html b/src/app/pages/contacts-page/contacts-page.html new file mode 100644 index 0000000..5400fab --- /dev/null +++ b/src/app/pages/contacts-page/contacts-page.html @@ -0,0 +1,66 @@ +
+
+

{{ 'contacts.title' | translate }}

+

{{ 'contacts.lead' | translate }}

+
+ +
+ + + +
+

{{ 'contacts.hours_title' | translate }}

+
+
+ {{ 'footer.support_label' | translate }} + 24/7 +
+
+ {{ 'footer.questions_label' | translate }} + 10:00–19:00 МСК +
+
+
+ +
+
diff --git a/src/app/pages/contacts-page/contacts-page.scss b/src/app/pages/contacts-page/contacts-page.scss new file mode 100644 index 0000000..76e3c95 --- /dev/null +++ b/src/app/pages/contacts-page/contacts-page.scss @@ -0,0 +1,146 @@ +:host { + display: block; + background: #f8fafc; + min-height: 100vh; +} + +.info-page { + max-width: 760px; + margin: 0 auto; + padding: 48px 24px 72px; + + @media (max-width: 600px) { + padding: 32px 16px 56px; + } + + &__hero { + margin-bottom: 48px; + border-bottom: 1px solid #e2e8f0; + padding-bottom: 32px; + } + + &__title { + font-size: 32px; + font-weight: 800; + color: #0f172a; + margin: 0 0 12px; + letter-spacing: -0.5px; + + @media (max-width: 600px) { font-size: 26px; } + } + + &__lead { + font-size: 17px; + line-height: 1.7; + color: #475569; + margin: 0; + } + + &__body { + display: flex; + flex-direction: column; + gap: 48px; + } +} + +.info-section { + &__title { + font-size: 20px; + font-weight: 700; + color: #1e293b; + margin: 0 0 14px; + } +} + +.contacts-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + + @media (max-width: 540px) { + grid-template-columns: 1fr; + } +} + +.contact-card { + display: flex; + align-items: center; + gap: 16px; + padding: 20px; + border-radius: 16px; + border: 1px solid #e2e8f0; + background: #fff; + text-decoration: none; + transition: border-color 0.15s, box-shadow 0.15s, background 0.15s; + + &:hover { + border-color: #93c5fd; + box-shadow: 0 4px 16px rgba(30, 64, 175, 0.08); + background: #f8fbff; + } + + &__icon { + font-size: 28px; + line-height: 1; + flex-shrink: 0; + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + } + + &__body { + display: flex; + flex-direction: column; + gap: 3px; + min-width: 0; + } + + &__label { + font-size: 11.5px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: #94a3b8; + } + + &__value { + font-size: 14.5px; + font-weight: 600; + color: #0f172a; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} + +.hours-table { + display: flex; + flex-direction: column; + gap: 12px; +} + +.hours-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 18px; + border-radius: 10px; + background: #f8fafc; + border: 1px solid #e2e8f0; + + &__label { + font-size: 14px; + color: #475569; + font-weight: 500; + } + + &__value { + font-size: 14px; + font-weight: 700; + color: #0f172a; + + &--green { color: #16a34a; } + } +} diff --git a/src/app/pages/contacts-page/contacts-page.ts b/src/app/pages/contacts-page/contacts-page.ts new file mode 100644 index 0000000..3f6bca9 --- /dev/null +++ b/src/app/pages/contacts-page/contacts-page.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { TranslatePipe } from '../../translate/translate.pipe'; + +@Component({ + selector: 'app-contacts-page', + imports: [TranslatePipe], + templateUrl: './contacts-page.html', + styleUrl: './contacts-page.scss' +}) +export class ContactsPage {} diff --git a/src/app/pages/create-page/create-page.html b/src/app/pages/create-page/create-page.html index ea25c0c..9f0f17c 100644 --- a/src/app/pages/create-page/create-page.html +++ b/src/app/pages/create-page/create-page.html @@ -30,6 +30,9 @@ + @@ -77,13 +80,15 @@ class="input-wrap__input" [ngModel]="amount()" (ngModelChange)="onAmountChange($event)" - min="1" + [min]="minAmount()" + [max]="maxAmount()" step="1" inputmode="numeric" placeholder="0" autofocus /> + {{ 'create.amount_hint' | translate }} {{ minAmount() }}–{{ maxAmount().toLocaleString('ru') }} ₽ @if (error()) { {{ error() }} } @@ -102,7 +107,7 @@ > - + + + @if (qrImageUrl()) { +
+ + QR + @if (qrPolling()) { +

{{ 'create.qr_waiting' | translate }}

+ } +
+ } @@ -47,7 +47,7 @@ step="1" inputmode="numeric" placeholder="0" - [readonly]="amountLoading() || fastcheckAmount() !== null" + [disabled]="true" /> @if (amountLoading()) { @@ -55,6 +55,28 @@ } + + @if (fastcheckAmount() !== null && !amountLoading()) { +
+ + +
+ } +
@@ -66,8 +88,9 @@ (ngModelChange)="onCodeChange($event)" [placeholder]="'fastcheck.code_placeholder' | translate" inputmode="numeric" - maxlength="5" + maxlength="6" autocomplete="one-time-code" + [disabled]="!codeEnabled()" /> @if (error()) { {{ error() }} diff --git a/src/app/pages/fastcheck-page/fastcheck-page.scss b/src/app/pages/fastcheck-page/fastcheck-page.scss index 0ce07d5..0dad22d 100644 --- a/src/app/pages/fastcheck-page/fastcheck-page.scss +++ b/src/app/pages/fastcheck-page/fastcheck-page.scss @@ -8,6 +8,40 @@ .input { flex: 1; min-width: 0; } } +.share-row { + display: flex; + gap: 8px; +} + +.share-btn { + flex: 1; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + height: 40px; + border-radius: 10px; + border: 1.5px solid #e2e8f0; + font-size: 13px; + font-weight: 600; + font-family: inherit; + cursor: pointer; + transition: background .15s, border-color .15s; + + &--email { + background: #f8fafc; + color: #475569; + &:hover { background: #e2e8f0; border-color: #cbd5e1; } + } + + &--tg { + background: #e7f3fe; + color: #0088cc; + border-color: #bfdbfe; + &:hover { background: #dbeafe; border-color: #93c5fd; } + } +} + .btn { display: inline-flex; align-items: center; diff --git a/src/app/pages/fastcheck-page/fastcheck-page.ts b/src/app/pages/fastcheck-page/fastcheck-page.ts index 4e9f59d..5748af9 100644 --- a/src/app/pages/fastcheck-page/fastcheck-page.ts +++ b/src/app/pages/fastcheck-page/fastcheck-page.ts @@ -5,6 +5,7 @@ import { HttpClient } from '@angular/common/http'; import { FastcheckService } from '../../fastcheck.service'; import { FASTCHECK_API } from '../../api'; import { TranslatePipe } from '../../translate/translate.pipe'; +import { TranslationService } from '../../translate/translation.service'; interface WebSessionResponse { sessionId: string; @@ -19,6 +20,7 @@ interface CheckFastcheckResponse { amount?: number; expiration: string; Status: boolean; + message?: string; } @Component({ @@ -31,6 +33,9 @@ export class FastcheckPage { private http = inject(HttpClient); private store = inject(FastcheckService); private router = inject(Router); + private i18n = inject(TranslationService); + + private t(key: string): string { return this.i18n.translate(key); } // Telegram bot used for the sign-in deep link. private readonly telegramBot = 'DexarSupport_bot'; @@ -38,6 +43,7 @@ export class FastcheckPage { fastcheckNumber = signal(''); fastcheckAmount = signal(null); fastcheckCode = signal(''); + codeEnabled = signal(false); error = signal(''); amountLoading = signal(false); @@ -52,7 +58,8 @@ export class FastcheckPage { canPay = computed(() => { const digits = this.fastcheckNumber().replace(/\D/g, ''); const codeDigits = this.fastcheckCode().replace(/\D/g, ''); - return digits.length === 12 && codeDigits.length === 5 && !this.amountLoading(); + return digits.length === 18 && codeDigits.length === 6 + && this.codeEnabled() && !this.amountLoading(); }); telegramLink = computed(() => { @@ -74,6 +81,18 @@ export class FastcheckPage { this.fastcheckNumber.set(created.fastcheck); this.fastcheckAmount.set(created.amount); this.fastcheckCode.set(created.code); + this.codeEnabled.set(true); + } + + // ?iid=xxxxxx-xxxxxx-xxxxxx — auto-fill and trigger lookup + const iidParam = new URLSearchParams(window.location.search).get('iid') ?? ''; + if (iidParam && !created) { + const digits = iidParam.replace(/\D/g, '').slice(0, 18); + const groups: string[] = []; + for (let i = 0; i < digits.length; i += 6) groups.push(digits.slice(i, i + 6)); + const masked = groups.join('-'); + this.fastcheckNumber.set(masked); + if (digits.length === 18) this.lookupFastcheck(masked); } } @@ -99,7 +118,7 @@ export class FastcheckPage { }, error: () => { this.popupLoading.set(false); - this.popupError.set('Не удалось создать сессию. Попробуйте ещё раз.'); + this.popupError.set(this.t('errors.session_failed')); } }); } @@ -154,12 +173,15 @@ export class FastcheckPage { next: () => { this.popupLoading.set(false); this.paid.set(true); - // Fire-and-forget merchant callback if a return_url is on the page. + // Fire DELETE to mark fastcheck as consumed on the merchant side. + this.http + .delete(`${FASTCHECK_API}/fastcheck/${encodeURIComponent(this.fastcheckNumber())}`) + .subscribe({ error: () => undefined }); this.fireMerchantCallback(); }, error: () => { this.popupLoading.set(false); - this.popupError.set('Не удалось принять платёж.'); + this.popupError.set(this.t('errors.payment_failed')); } }); } @@ -180,33 +202,31 @@ export class FastcheckPage { this.fastcheckAmount.set(value); } - /** Mask fastcheck number as XXXX-XXXX-XXXX, allow only digits. */ + /** Mask fastcheck number as XXXXXX-XXXXXX-XXXXXX, allow only digits. */ onNumberChange(raw: string): void { - const digits = (raw ?? '').replace(/\D/g, '').slice(0, 12); + const digits = (raw ?? '').replace(/\D/g, '').slice(0, 18); const groups: string[] = []; - for (let i = 0; i < digits.length; i += 4) { - groups.push(digits.slice(i, i + 4)); + for (let i = 0; i < digits.length; i += 6) { + groups.push(digits.slice(i, i + 6)); } const masked = groups.join('-'); this.fastcheckNumber.set(masked); this.error.set(''); - // If number became incomplete, drop the previously fetched amount so the - // user doesn't see a stale value tied to a different (now-edited) number. - if (digits.length < 12 && this.lastLookedUpNumber) { + if (digits.length < 18 && this.lastLookedUpNumber) { this.fastcheckAmount.set(null); + this.codeEnabled.set(false); this.lastLookedUpNumber = ''; } - // Auto-lookup when 12 digits are entered (and we haven't already looked it up). - if (digits.length === 12 && masked !== this.lastLookedUpNumber) { + if (digits.length === 18 && masked !== this.lastLookedUpNumber) { this.lookupFastcheck(masked); } } - /** Allow only digits, max 5, in the code field. */ + /** Allow only digits, max 6, in the code field. */ onCodeChange(raw: string): void { - const digits = (raw ?? '').replace(/\D/g, '').slice(0, 5); + const digits = (raw ?? '').replace(/\D/g, '').slice(0, 6); this.fastcheckCode.set(digits); this.error.set(''); } @@ -215,28 +235,42 @@ export class FastcheckPage { this.lastLookedUpNumber = number; this.amountLoading.set(true); this.fastcheckAmount.set(null); + this.codeEnabled.set(false); - // GET /fastcheck — body in GET is non-standard; many HTTP libs strip it. - // The backend should accept ?fastcheck= as a query param too. We send both. this.http - .request('GET', `${FASTCHECK_API}/fastcheck`, { - body: { fastcheck: number }, - params: { fastcheck: number } - }) + .get(`${FASTCHECK_API}/fastcheck/${encodeURIComponent(number)}`) .subscribe({ next: (res) => { this.amountLoading.set(false); if (res?.Status && typeof res.amount === 'number') { this.fastcheckAmount.set(res.amount); - } else if (res?.Status === false) { - this.error.set('Платёж не найден или просрочен.'); + this.codeEnabled.set(true); + } else { + this.error.set(res?.message ?? this.t('errors.not_found')); + this.lastLookedUpNumber = ''; } }, - error: () => { + error: (err) => { this.amountLoading.set(false); - this.error.set('Не удалось проверить номер. Попробуйте ещё раз.'); + const serverMsg: string | undefined = err?.error?.message; + this.error.set(serverMsg ?? this.t('errors.lookup_failed')); this.lastLookedUpNumber = ''; } }); } + + shareByEmail(): void { + const num = this.fastcheckNumber(); + const amount = this.fastcheckAmount(); + const subject = encodeURIComponent('fastCHECK'); + const body = encodeURIComponent(`Номер: ${num}\nСумма: ${amount} ₽\nhttps://qr.vitanova.network/`); + window.open(`mailto:?subject=${subject}&body=${body}`, '_blank'); + } + + shareByTelegram(): void { + const num = this.fastcheckNumber(); + const amount = this.fastcheckAmount(); + const text = encodeURIComponent(`fastCHECK: ${num} — ${amount} ₽`); + window.open(`https://t.me/share/url?url=https%3A%2F%2Fqr.vitanova.network%2F&text=${text}`, '_blank'); + } } diff --git a/src/app/pages/legacy-pay-page/legacy-pay-page.ts b/src/app/pages/legacy-pay-page/legacy-pay-page.ts index 8c5047e..36f9f06 100644 --- a/src/app/pages/legacy-pay-page/legacy-pay-page.ts +++ b/src/app/pages/legacy-pay-page/legacy-pay-page.ts @@ -3,6 +3,7 @@ import { FormsModule } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { HttpClient } from '@angular/common/http'; import { TranslatePipe } from '../../translate/translate.pipe'; +import { TranslationService } from '../../translate/translation.service'; interface LegacyPayResponse { payload?: string; @@ -25,6 +26,9 @@ interface LegacyPayResponse { export class LegacyPayPage { private http = inject(HttpClient); private route = inject(ActivatedRoute); + private i18n = inject(TranslationService); + + private t(key: string): string { return this.i18n.translate(key); } private readonly LEGACY_API = 'https://qr.vitanova.network:567/qr'; @@ -57,9 +61,9 @@ export class LegacyPayPage { pay(): void { if (!this.canPay()) { if (!this.paymentId()) { - this.error.set('Не указан идентификатор платежа (параметр id)'); + this.error.set(this.t('errors.not_found')); } else { - this.error.set('Введите корректную сумму'); + this.error.set(this.t('errors.invalid_amount')); } return; } @@ -80,12 +84,12 @@ export class LegacyPayPage { if (res?.payload) { window.location.href = res.payload; } else { - this.error.set('Сервер не вернул ссылку для оплаты.'); + this.error.set(this.t('errors.payment_failed')); } }, error: () => { this.loading.set(false); - this.error.set('Ошибка при создании платежа. Попробуйте ещё раз.'); + this.error.set(this.t('errors.lookup_failed')); } }); } diff --git a/src/app/pages/partners-page/partners-page.html b/src/app/pages/partners-page/partners-page.html new file mode 100644 index 0000000..314884c --- /dev/null +++ b/src/app/pages/partners-page/partners-page.html @@ -0,0 +1,26 @@ +
+
+

{{ 'partners.title' | translate }}

+

{{ 'partners.lead' | translate }}

+
+ +
+ @for (p of partners; track p.name) { +
+ +
+ {{ p.category | translate }} +

{{ p.name }}

+

📍 {{ p.city }}

+

{{ p.desc | translate }}

+
+
+ } +
+ +
+

{{ 'partners.cta_title' | translate }}

+

{{ 'partners.cta_text' | translate }}

+ {{ 'partners.cta_btn' | translate }} +
+
diff --git a/src/app/pages/partners-page/partners-page.scss b/src/app/pages/partners-page/partners-page.scss new file mode 100644 index 0000000..b45f9d0 --- /dev/null +++ b/src/app/pages/partners-page/partners-page.scss @@ -0,0 +1,146 @@ +:host { + display: block; + background: #f8fafc; + min-height: 100vh; +} + +.info-page { + max-width: 900px; + margin: 0 auto; + padding: 48px 24px 72px; + + @media (max-width: 600px) { + padding: 32px 16px 56px; + } + + &__hero { + margin-bottom: 40px; + border-bottom: 1px solid #e2e8f0; + padding-bottom: 32px; + } + + &__title { + font-size: 32px; + font-weight: 800; + color: #0f172a; + margin: 0 0 12px; + letter-spacing: -0.5px; + + @media (max-width: 600px) { font-size: 26px; } + } + + &__lead { + font-size: 17px; + line-height: 1.7; + color: #475569; + margin: 0; + } +} + +.partners-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 20px; + margin-bottom: 56px; +} + +.partner-card { + display: flex; + gap: 16px; + padding: 22px 20px; + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 16px; + transition: border-color 0.15s, box-shadow 0.15s; + + &:hover { + border-color: #93c5fd; + box-shadow: 0 4px 16px rgba(30, 64, 175, 0.08); + } + + &__logo { + font-size: 36px; + line-height: 1; + flex-shrink: 0; + width: 52px; + height: 52px; + display: flex; + align-items: center; + justify-content: center; + background: #f1f5f9; + border-radius: 12px; + } + + &__body { + display: flex; + flex-direction: column; + gap: 4px; + } + + &__cat { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: #1e40af; + } + + &__name { + font-size: 16px; + font-weight: 700; + color: #0f172a; + margin: 0; + } + + &__city { + font-size: 13px; + color: #64748b; + margin: 0; + } + + &__desc { + font-size: 13.5px; + line-height: 1.6; + color: #475569; + margin: 4px 0 0; + } +} + +.partners-cta { + text-align: center; + padding: 40px 24px; + background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%); + border-radius: 20px; + border: 1px solid #bfdbfe; + + &__title { + font-size: 22px; + font-weight: 800; + color: #1e3a8a; + margin: 0 0 12px; + } + + &__text { + font-size: 15px; + line-height: 1.7; + color: #3b5998; + margin: 0 0 24px; + max-width: 480px; + margin-inline: auto; + margin-bottom: 24px; + } + + &__btn { + display: inline-block; + padding: 12px 28px; + background: #1e40af; + color: #fff; + border-radius: 10px; + font-size: 15px; + font-weight: 700; + text-decoration: none; + transition: background 0.15s; + + &:hover { background: #1d3a9f; } + } +} diff --git a/src/app/pages/partners-page/partners-page.ts b/src/app/pages/partners-page/partners-page.ts new file mode 100644 index 0000000..0d501d1 --- /dev/null +++ b/src/app/pages/partners-page/partners-page.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { TranslatePipe } from '../../translate/translate.pipe'; + +interface Partner { + name: string; + category: string; + city: string; + logo: string; // emoji placeholder until real logos are provided + desc: string; +} + +@Component({ + selector: 'app-partners-page', + imports: [RouterLink, TranslatePipe], + templateUrl: './partners-page.html', + styleUrl: './partners-page.scss' +}) +export class PartnersPage { + partners: Partner[] = [ + { name: 'Vitanova Exchange', category: 'partners.cat_finance', city: 'Ереван', logo: '🏦', desc: 'partners.p1_desc' }, + { name: 'ForEx.am', category: 'partners.cat_finance', city: 'Ереван', logo: '💱', desc: 'partners.p2_desc' }, + { name: 'Dexar Market', category: 'partners.cat_retail', city: 'Москва', logo: '🛒', desc: 'partners.p3_desc' }, + { name: 'City Hotel Yerevan', category: 'partners.cat_hotels', city: 'Ереван', logo: '🏨', desc: 'partners.p4_desc' }, + ]; +} diff --git a/src/app/site-header/site-header.html b/src/app/site-header/site-header.html index 25bcc4f..2f20ca2 100644 --- a/src/app/site-header/site-header.html +++ b/src/app/site-header/site-header.html @@ -11,16 +11,40 @@ - -
- - - + +
+ + @if (langOpen()) { +
+ @for (lang of langs; track lang.code) { + + } +
+ }
@@ -42,17 +66,33 @@
- + @if (menuOpen()) { - +
+ +
} diff --git a/src/app/site-header/site-header.scss b/src/app/site-header/site-header.scss index ea5e659..2db627d 100644 --- a/src/app/site-header/site-header.scss +++ b/src/app/site-header/site-header.scss @@ -71,19 +71,11 @@ } } - &__langs { - display: flex; - align-items: center; - gap: 2px; - margin-left: 8px; - - @media (max-width: 600px) { - display: none; - } - } - &__lang { - padding: 5px 8px; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 10px; border-radius: 6px; border: none; background: transparent; @@ -107,6 +99,8 @@ display: flex; gap: 4px; padding: 8px 14px 4px; + border-top: 1px solid #f1f5f9; + margin-top: 4px; } &__burger { @@ -132,25 +126,9 @@ } } - &__mobile-menu { - border-top: 1px solid #e2e8f0; - display: flex; - flex-direction: column; - padding: 8px 12px 12px; - background: #fff; - } + &__mobile-menu { display: none; } // replaced by .mobile-overlay / .mobile-panel - &__mobile-link { - padding: 12px 14px; - border-radius: 10px; - font-size: 15px; - font-weight: 500; - color: #0f172a; - text-decoration: none; - transition: background 0.15s; - - &:hover { background: #f1f5f9; } - } + &__mobile-link { display: none; } } // Wordmark colours @@ -167,3 +145,180 @@ text-transform: uppercase; letter-spacing: 0.03em; } + +// Language dropdown +.lang-select { + position: relative; + flex-shrink: 0; + + @media (max-width: 600px) { + display: none; + } + + &__trigger { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 7px 10px; + border-radius: 8px; + border: 1px solid #e2e8f0; + background: #fff; + font-size: 13px; + font-weight: 600; + color: #334155; + cursor: pointer; + font-family: inherit; + transition: border-color 0.15s, background 0.15s; + white-space: nowrap; + + &:hover { background: #f8fafc; border-color: #cbd5e1; } + } + + &--open &__trigger { + background: #f8fafc; + border-color: #94a3b8; + } + + &__flag { width: 20px; height: 20px; object-fit: cover; border-radius: 2px; flex-shrink: 0; } + + &__code { font-size: 12px; font-weight: 700; letter-spacing: 0.05em; } + + &__chevron { + color: #94a3b8; + transition: transform 0.2s; + } + + &--open &__chevron { transform: rotate(180deg); } + + &__dropdown { + position: absolute; + top: calc(100% + 6px); + right: 0; + min-width: 160px; + background: #fff; + border: 1px solid #e2e8f0; + border-radius: 12px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); + overflow: hidden; + z-index: 1000; + animation: dropdown-in 0.12s ease; + } + + &__option { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 11px 14px; + border: none; + background: transparent; + font-family: inherit; + font-size: 14px; + font-weight: 500; + color: #334155; + cursor: pointer; + text-align: left; + transition: background 0.12s; + + &:hover { background: #f8fafc; } + + &--active { color: #1e40af; background: #eff6ff; } + } + + &__name { flex: 1; } + + &__check { color: #1e40af; margin-left: auto; flex-shrink: 0; } +} + +@keyframes dropdown-in { + from { opacity: 0; transform: translateY(-6px); } + to { opacity: 1; transform: translateY(0); } +} + +// ── Mobile overlay + drawer ────────────────────────────────────── +.mobile-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.45); + z-index: 998; + animation: overlay-in 0.2s ease; +} + +.mobile-panel { + position: fixed; + top: 0; + right: 0; + height: 100%; + width: min(300px, 85vw); + background: #fff; + z-index: 999; + display: flex; + flex-direction: column; + gap: 2px; + overflow-y: auto; + animation: panel-in 0.22s cubic-bezier(0.4, 0, 0.2, 1); + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 18px 20px 16px; + border-bottom: 1px solid #e2e8f0; + flex-shrink: 0; + } + + &__title { + font-size: 16px; + font-weight: 700; + color: #1e40af; + letter-spacing: 0.02em; + } + + &__close { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 8px; + border: none; + background: transparent; + color: #64748b; + cursor: pointer; + transition: background 0.15s, color 0.15s; + font-family: inherit; + + &:hover { background: #f1f5f9; color: #0f172a; } + } + + &__link { + display: block; + padding: 14px 20px; + font-size: 15px; + font-weight: 500; + color: #0f172a; + text-decoration: none; + transition: background 0.12s; + border-radius: 0; + + &:hover { background: #f8fafc; } + } + + &__langs { + display: flex; + gap: 6px; + padding: 12px 20px 16px; + border-top: 1px solid #f1f5f9; + margin-top: auto; + } +} + +@keyframes overlay-in { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes panel-in { + from { transform: translateX(100%); } + to { transform: translateX(0); } +} diff --git a/src/app/site-header/site-header.ts b/src/app/site-header/site-header.ts index 552f410..7283803 100644 --- a/src/app/site-header/site-header.ts +++ b/src/app/site-header/site-header.ts @@ -1,11 +1,14 @@ -import { Component, inject, signal } from '@angular/core'; +import { UpperCasePipe } from '@angular/common'; +import { Component, HostListener, inject, signal } from '@angular/core'; import { RouterLink } from '@angular/router'; import { TranslatePipe } from '../translate/translate.pipe'; import { TranslationService, Lang } from '../translate/translation.service'; +interface LangOption { code: Lang; label: string; flag: string; } + @Component({ selector: 'app-site-header', - imports: [RouterLink, TranslatePipe], + imports: [RouterLink, TranslatePipe, UpperCasePipe], templateUrl: './site-header.html', styleUrl: './site-header.scss' }) @@ -13,9 +16,33 @@ export class SiteHeader { private i18n = inject(TranslationService); menuOpen = signal(false); + langOpen = signal(false); currentLang = this.i18n.currentLang; + langs: LangOption[] = [ + { code: 'ru', label: 'Русский', flag: '/flags/ru.svg' }, + { code: 'en', label: 'English', flag: '/flags/en.svg' }, + { code: 'hy', label: 'Հայերեն', flag: '/flags/arm.svg' }, + ]; + + get activeLang(): LangOption { + return this.langs.find(l => l.code === this.currentLang()) ?? this.langs[0]; + } + toggleMenu(): void { this.menuOpen.update(v => !v); } closeMenu(): void { this.menuOpen.set(false); } - setLang(lang: Lang): void { this.i18n.setLanguage(lang); } + toggleLang(): void { this.langOpen.update(v => !v); } + closeLang(): void { this.langOpen.set(false); } + + setLang(lang: Lang): void { + this.i18n.setLanguage(lang); + this.langOpen.set(false); + } + + @HostListener('document:click', ['$event.target']) + onDocClick(target: EventTarget | null): void { + if (!(target instanceof HTMLElement) || !target.closest('.lang-select')) { + this.langOpen.set(false); + } + } }