changes
This commit is contained in:
2
dist/index.html
vendored
2
dist/index.html
vendored
@@ -14,5 +14,5 @@
|
|||||||
<link rel="stylesheet" href="styles.css"></head>
|
<link rel="stylesheet" href="styles.css"></head>
|
||||||
<body>
|
<body>
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
<link rel="modulepreload" href="chunk-XQ4WH5CY.js"><script src="main.js" type="module"></script></body>
|
<link rel="modulepreload" href="chunk-XFC3HHJM.js"><link rel="modulepreload" href="chunk-AUOHPQWG.js"><script src="main.js" type="module"></script></body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
5
public/alipay.svg
Normal file
5
public/alipay.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
|
||||||
|
<rect width="48" height="48" rx="8" fill="#1677FF"/>
|
||||||
|
<text x="24" y="34" font-family="Arial,Helvetica,sans-serif" font-size="26" font-weight="900"
|
||||||
|
text-anchor="middle" fill="#fff">A</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 269 B |
1
public/flags/arm.svg
Normal file
1
public/flags/arm.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="#102f9b" d="M1 11H31V21H1z"></path><path d="M5,4H27c2.208,0,4,1.792,4,4v4H1v-4c0-2.208,1.792-4,4-4Z" fill="#c82a20"></path><path d="M5,20H27c2.208,0,4,1.792,4,4v4H1v-4c0-2.208,1.792-4,4-4Z" transform="rotate(180 16 24)" fill="#e8ad3b"></path><path d="M27,4H5c-2.209,0-4,1.791-4,4V24c0,2.209,1.791,4,4,4H27c2.209,0,4-1.791,4-4V8c0-2.209-1.791-4-4-4Zm3,20c0,1.654-1.346,3-3,3H5c-1.654,0-3-1.346-3-3V8c0-1.654,1.346-3,3-3H27c1.654,0,3,1.346,3,3V24Z" opacity=".15"></path><path d="M27,5H5c-1.657,0-3,1.343-3,3v1c0-1.657,1.343-3,3-3H27c1.657,0,3,1.343,3,3v-1c0-1.657-1.343-3-3-3Z" fill="#fff" opacity=".2"></path></svg>
|
||||||
|
After Width: | Height: | Size: 709 B |
1
public/flags/en.svg
Normal file
1
public/flags/en.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><rect x="1" y="4" width="30" height="24" rx="4" ry="4" fill="#fff"></rect><path d="M1.638,5.846H30.362c-.711-1.108-1.947-1.846-3.362-1.846H5c-1.414,0-2.65,.738-3.362,1.846Z" fill="#a62842"></path><path d="M2.03,7.692c-.008,.103-.03,.202-.03,.308v1.539H31v-1.539c0-.105-.022-.204-.03-.308H2.03Z" fill="#a62842"></path><path fill="#a62842" d="M2 11.385H31V13.231H2z"></path><path fill="#a62842" d="M2 15.077H31V16.923000000000002H2z"></path><path fill="#a62842" d="M1 18.769H31V20.615H1z"></path><path d="M1,24c0,.105,.023,.204,.031,.308H30.969c.008-.103,.031-.202,.031-.308v-1.539H1v1.539Z" fill="#a62842"></path><path d="M30.362,26.154H1.638c.711,1.108,1.947,1.846,3.362,1.846H27c1.414,0,2.65-.738,3.362-1.846Z" fill="#a62842"></path><path d="M5,4h11v12.923H1V8c0-2.208,1.792-4,4-4Z" fill="#102d5e"></path><path d="M27,4H5c-2.209,0-4,1.791-4,4V24c0,2.209,1.791,4,4,4H27c2.209,0,4-1.791,4-4V8c0-2.209-1.791-4-4-4Zm3,20c0,1.654-1.346,3-3,3H5c-1.654,0-3-1.346-3-3V8c0-1.654,1.346-3,3-3H27c1.654,0,3,1.346,3,3V24Z" opacity=".15"></path><path d="M27,5H5c-1.657,0-3,1.343-3,3v1c0-1.657,1.343-3,3-3H27c1.657,0,3,1.343,3,3v-1c0-1.657-1.343-3-3-3Z" fill="#fff" opacity=".2"></path><path fill="#fff" d="M4.601 7.463L5.193 7.033 4.462 7.033 4.236 6.338 4.01 7.033 3.279 7.033 3.87 7.463 3.644 8.158 4.236 7.729 4.827 8.158 4.601 7.463z"></path><path fill="#fff" d="M7.58 7.463L8.172 7.033 7.441 7.033 7.215 6.338 6.989 7.033 6.258 7.033 6.849 7.463 6.623 8.158 7.215 7.729 7.806 8.158 7.58 7.463z"></path><path fill="#fff" d="M10.56 7.463L11.151 7.033 10.42 7.033 10.194 6.338 9.968 7.033 9.237 7.033 9.828 7.463 9.603 8.158 10.194 7.729 10.785 8.158 10.56 7.463z"></path><path fill="#fff" d="M6.066 9.283L6.658 8.854 5.927 8.854 5.701 8.158 5.475 8.854 4.744 8.854 5.335 9.283 5.109 9.979 5.701 9.549 6.292 9.979 6.066 9.283z"></path><path fill="#fff" d="M9.046 9.283L9.637 8.854 8.906 8.854 8.68 8.158 8.454 8.854 7.723 8.854 8.314 9.283 8.089 9.979 8.68 9.549 9.271 9.979 9.046 9.283z"></path><path fill="#fff" d="M12.025 9.283L12.616 8.854 11.885 8.854 11.659 8.158 11.433 8.854 10.702 8.854 11.294 9.283 11.068 9.979 11.659 9.549 12.251 9.979 12.025 9.283z"></path><path fill="#fff" d="M6.066 12.924L6.658 12.494 5.927 12.494 5.701 11.799 5.475 12.494 4.744 12.494 5.335 12.924 5.109 13.619 5.701 13.19 6.292 13.619 6.066 12.924z"></path><path fill="#fff" d="M9.046 12.924L9.637 12.494 8.906 12.494 8.68 11.799 8.454 12.494 7.723 12.494 8.314 12.924 8.089 13.619 8.68 13.19 9.271 13.619 9.046 12.924z"></path><path fill="#fff" d="M12.025 12.924L12.616 12.494 11.885 12.494 11.659 11.799 11.433 12.494 10.702 12.494 11.294 12.924 11.068 13.619 11.659 13.19 12.251 13.619 12.025 12.924z"></path><path fill="#fff" d="M13.539 7.463L14.13 7.033 13.399 7.033 13.173 6.338 12.947 7.033 12.216 7.033 12.808 7.463 12.582 8.158 13.173 7.729 13.765 8.158 13.539 7.463z"></path><path fill="#fff" d="M4.601 11.104L5.193 10.674 4.462 10.674 4.236 9.979 4.01 10.674 3.279 10.674 3.87 11.104 3.644 11.799 4.236 11.369 4.827 11.799 4.601 11.104z"></path><path fill="#fff" d="M7.58 11.104L8.172 10.674 7.441 10.674 7.215 9.979 6.989 10.674 6.258 10.674 6.849 11.104 6.623 11.799 7.215 11.369 7.806 11.799 7.58 11.104z"></path><path fill="#fff" d="M10.56 11.104L11.151 10.674 10.42 10.674 10.194 9.979 9.968 10.674 9.237 10.674 9.828 11.104 9.603 11.799 10.194 11.369 10.785 11.799 10.56 11.104z"></path><path fill="#fff" d="M13.539 11.104L14.13 10.674 13.399 10.674 13.173 9.979 12.947 10.674 12.216 10.674 12.808 11.104 12.582 11.799 13.173 11.369 13.765 11.799 13.539 11.104z"></path><path fill="#fff" d="M4.601 14.744L5.193 14.315 4.462 14.315 4.236 13.619 4.01 14.315 3.279 14.315 3.87 14.744 3.644 15.44 4.236 15.01 4.827 15.44 4.601 14.744z"></path><path fill="#fff" d="M7.58 14.744L8.172 14.315 7.441 14.315 7.215 13.619 6.989 14.315 6.258 14.315 6.849 14.744 6.623 15.44 7.215 15.01 7.806 15.44 7.58 14.744z"></path><path fill="#fff" d="M10.56 14.744L11.151 14.315 10.42 14.315 10.194 13.619 9.968 14.315 9.237 14.315 9.828 14.744 9.603 15.44 10.194 15.01 10.785 15.44 10.56 14.744z"></path><path fill="#fff" d="M13.539 14.744L14.13 14.315 13.399 14.315 13.173 13.619 12.947 14.315 12.216 14.315 12.808 14.744 12.582 15.44 13.173 15.01 13.765 15.44 13.539 14.744z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.2 KiB |
1
public/flags/ru.svg
Normal file
1
public/flags/ru.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="#1435a1" d="M1 11H31V21H1z"></path><path d="M5,4H27c2.208,0,4,1.792,4,4v4H1v-4c0-2.208,1.792-4,4-4Z" fill="#fff"></path><path d="M5,20H27c2.208,0,4,1.792,4,4v4H1v-4c0-2.208,1.792-4,4-4Z" transform="rotate(180 16 24)" fill="#c53a28"></path><path d="M27,4H5c-2.209,0-4,1.791-4,4V24c0,2.209,1.791,4,4,4H27c2.209,0,4-1.791,4-4V8c0-2.209-1.791-4-4-4Zm3,20c0,1.654-1.346,3-3,3H5c-1.654,0-3-1.346-3-3V8c0-1.654,1.346-3,3-3H27c1.654,0,3,1.346,3,3V24Z" opacity=".15"></path><path d="M27,5H5c-1.657,0-3,1.343-3,3v1c0-1.657,1.343-3,3-3H27c1.657,0,3,1.343,3,3v-1c0-1.657-1.343-3-3-3Z" fill="#fff" opacity=".2"></path></svg>
|
||||||
|
After Width: | Height: | Size: 706 B |
@@ -2,10 +2,12 @@
|
|||||||
"header": {
|
"header": {
|
||||||
"nav_about": "About",
|
"nav_about": "About",
|
||||||
"nav_contacts": "Contacts",
|
"nav_contacts": "Contacts",
|
||||||
|
"nav_partners": "Partners",
|
||||||
"nav_support": "Support",
|
"nav_support": "Support",
|
||||||
"aria_nav": "Navigation",
|
"aria_nav": "Navigation",
|
||||||
"aria_menu": "Mobile menu",
|
"aria_menu": "Mobile menu",
|
||||||
"aria_burger": "Menu"
|
"aria_burger": "Menu",
|
||||||
|
"aria_close": "Close menu"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"desc": "An innovative virtual check service for individuals. Create digital checks online and cash them out at partner bank ATMs 24/7.",
|
"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_label": "Note",
|
||||||
"note_placeholder": "Reason for payment...",
|
"note_placeholder": "Reason for payment...",
|
||||||
"creating": "Creating…",
|
"creating": "Creating…",
|
||||||
"create_btn": "Create"
|
"create_btn": "Create",
|
||||||
|
"amount_hint": "Allowed amount:"
|
||||||
},
|
},
|
||||||
"sbp": {
|
"sbp": {
|
||||||
"title": "Pay via SBP",
|
"title": "Pay via SBP",
|
||||||
@@ -67,7 +70,57 @@
|
|||||||
"pay_loading": "Please wait...",
|
"pay_loading": "Please wait...",
|
||||||
"pay_btn": "Proceed to payment"
|
"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": {
|
"common": {
|
||||||
"secure": "Secure connection"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
"header": {
|
"header": {
|
||||||
"nav_about": "Ծառայության մասին",
|
"nav_about": "Ծառայության մասին",
|
||||||
"nav_contacts": "Կապ",
|
"nav_contacts": "Կապ",
|
||||||
|
"nav_partners": "Գործընկերներ",
|
||||||
"nav_support": "Աջակցություն",
|
"nav_support": "Աջակցություն",
|
||||||
"aria_nav": "Նավիգացիա",
|
"aria_nav": "Նավիգացիա",
|
||||||
"aria_menu": "Բջջային ընտրացանկ",
|
"aria_menu": "Բջջային ընտրացանկ",
|
||||||
"aria_burger": "Ընտրացանկ"
|
"aria_burger": "Ընտրացանկ",
|
||||||
|
"aria_close": "Փակել ընտրացանկը"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"desc": "Ֆիզիկական անձանց համար վիրտուալ չեկերի նորարարական ծառայություն: Ստեղծեք թվային չեկեր առցանց և կանխիկացրեք դրանք գործընկեր բանկերի բանկոմատներում 24/7:",
|
"desc": "Ֆիզիկական անձանց համար վիրտուալ չեկերի նորարարական ծառայություն: Ստեղծեք թվային չեկեր առցանց և կանխիկացրեք դրանք գործընկեր բանկերի բանկոմատներում 24/7:",
|
||||||
@@ -55,7 +57,8 @@
|
|||||||
"note_label": "Նշում",
|
"note_label": "Նշում",
|
||||||
"note_placeholder": "Վճարման պատճառ...",
|
"note_placeholder": "Վճարման պատճառ...",
|
||||||
"creating": "Ստեղծվում է…",
|
"creating": "Ստեղծվում է…",
|
||||||
"create_btn": "Ստեղծել"
|
"create_btn": "Ստեղծել",
|
||||||
|
"amount_hint": "Թույլատրելի գումար՝"
|
||||||
},
|
},
|
||||||
"sbp": {
|
"sbp": {
|
||||||
"title": "Վճարել SBP-ով",
|
"title": "Վճարել SBP-ով",
|
||||||
@@ -67,7 +70,57 @@
|
|||||||
"pay_loading": "Սպասեք...",
|
"pay_loading": "Սպասեք...",
|
||||||
"pay_btn": "Անցնել վճարմանը"
|
"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": {
|
"common": {
|
||||||
"secure": "Անվտանգ կապ"
|
"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": "Կապվեք մեզ հետ"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,12 @@
|
|||||||
"header": {
|
"header": {
|
||||||
"nav_about": "О сервисе",
|
"nav_about": "О сервисе",
|
||||||
"nav_contacts": "Контакты",
|
"nav_contacts": "Контакты",
|
||||||
|
"nav_partners": "Партнёры",
|
||||||
"nav_support": "Поддержка",
|
"nav_support": "Поддержка",
|
||||||
"aria_nav": "Навигация",
|
"aria_nav": "Навигация",
|
||||||
"aria_menu": "Мобильное меню",
|
"aria_menu": "Мобильное меню",
|
||||||
"aria_burger": "Меню"
|
"aria_burger": "Меню",
|
||||||
|
"aria_close": "Закрыть меню"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"desc": "Инновационный сервис виртуальных чеков для физических лиц. Создавайте цифровые чеки онлайн и обналичивайте их через банкоматы банков-партнёров 24/7.",
|
"desc": "Инновационный сервис виртуальных чеков для физических лиц. Создавайте цифровые чеки онлайн и обналичивайте их через банкоматы банков-партнёров 24/7.",
|
||||||
@@ -55,7 +57,8 @@
|
|||||||
"note_label": "Примечание",
|
"note_label": "Примечание",
|
||||||
"note_placeholder": "Причина платежа...",
|
"note_placeholder": "Причина платежа...",
|
||||||
"creating": "Создание…",
|
"creating": "Создание…",
|
||||||
"create_btn": "Создать"
|
"create_btn": "Создать",
|
||||||
|
"amount_hint": "Допустимая сумма:"
|
||||||
},
|
},
|
||||||
"sbp": {
|
"sbp": {
|
||||||
"title": "Оплата через СБП",
|
"title": "Оплата через СБП",
|
||||||
@@ -67,7 +70,57 @@
|
|||||||
"pay_loading": "Подождите...",
|
"pay_loading": "Подождите...",
|
||||||
"pay_btn": "Перейти к оплате"
|
"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": {
|
"common": {
|
||||||
"secure": "Защищённое соединение"
|
"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": "Связаться с нами"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,6 @@ export const FASTCHECK_API = 'https://api.fastcheck.store';
|
|||||||
|
|
||||||
// Legacy QR endpoint kept for the SBP amount → payload redirect flow.
|
// Legacy QR endpoint kept for the SBP amount → payload redirect flow.
|
||||||
export const QR_API = 'https://qr.vitanova.network:567/qr';
|
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';
|
||||||
|
|||||||
@@ -17,5 +17,20 @@ export const routes: Routes = [
|
|||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('./pages/create-page/create-page').then((m) => m.CreatePage)
|
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: '' }
|
{ path: '**', redirectTo: '' }
|
||||||
];
|
];
|
||||||
|
|||||||
40
src/app/pages/about-page/about-page.html
Normal file
40
src/app/pages/about-page/about-page.html
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<div class="info-page">
|
||||||
|
<div class="info-page__hero">
|
||||||
|
<h1 class="info-page__title">{{ 'about.title' | translate }}</h1>
|
||||||
|
<p class="info-page__lead">{{ 'about.lead' | translate }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-page__body">
|
||||||
|
|
||||||
|
<section class="info-section">
|
||||||
|
<h2 class="info-section__title">{{ 'about.what_title' | translate }}</h2>
|
||||||
|
<p class="info-section__text">{{ 'about.what_text' | translate }}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="info-section">
|
||||||
|
<h2 class="info-section__title">{{ 'about.how_title' | translate }}</h2>
|
||||||
|
<ol class="info-section__steps">
|
||||||
|
<li>{{ 'about.step1' | translate }}</li>
|
||||||
|
<li>{{ 'about.step2' | translate }}</li>
|
||||||
|
<li>{{ 'about.step3' | translate }}</li>
|
||||||
|
<li>{{ 'about.step4' | translate }}</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="info-section">
|
||||||
|
<h2 class="info-section__title">{{ 'about.why_title' | translate }}</h2>
|
||||||
|
<ul class="info-section__list">
|
||||||
|
<li>{{ 'about.why1' | translate }}</li>
|
||||||
|
<li>{{ 'about.why2' | translate }}</li>
|
||||||
|
<li>{{ 'about.why3' | translate }}</li>
|
||||||
|
<li>{{ 'about.why4' | translate }}</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="info-section">
|
||||||
|
<h2 class="info-section__title">{{ 'about.company_title' | translate }}</h2>
|
||||||
|
<p class="info-section__text">{{ 'about.company_text' | translate }}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
75
src/app/pages/about-page/about-page.scss
Normal file
75
src/app/pages/about-page/about-page.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/app/pages/about-page/about-page.ts
Normal file
10
src/app/pages/about-page/about-page.ts
Normal file
@@ -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 {}
|
||||||
66
src/app/pages/contacts-page/contacts-page.html
Normal file
66
src/app/pages/contacts-page/contacts-page.html
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<div class="info-page">
|
||||||
|
<div class="info-page__hero">
|
||||||
|
<h1 class="info-page__title">{{ 'contacts.title' | translate }}</h1>
|
||||||
|
<p class="info-page__lead">{{ 'contacts.lead' | translate }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-page__body">
|
||||||
|
|
||||||
|
<div class="contacts-grid">
|
||||||
|
|
||||||
|
<!-- Phone Russia -->
|
||||||
|
<a class="contact-card" href="tel:+79299037443">
|
||||||
|
<div class="contact-card__icon">🇷🇺</div>
|
||||||
|
<div class="contact-card__body">
|
||||||
|
<span class="contact-card__label">{{ 'contacts.ru_label' | translate }}</span>
|
||||||
|
<span class="contact-card__value">+7 (929) 903-74-43</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Phone Armenia -->
|
||||||
|
<a class="contact-card" href="tel:+37498632421">
|
||||||
|
<div class="contact-card__icon">🇦🇲</div>
|
||||||
|
<div class="contact-card__body">
|
||||||
|
<span class="contact-card__label">{{ 'contacts.am_label' | translate }}</span>
|
||||||
|
<span class="contact-card__value">+374 98 632421</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Email -->
|
||||||
|
<a class="contact-card" href="mailto:info@viaexport.store">
|
||||||
|
<div class="contact-card__icon">✉️</div>
|
||||||
|
<div class="contact-card__body">
|
||||||
|
<span class="contact-card__label">{{ 'contacts.email_label' | translate }}</span>
|
||||||
|
<span class="contact-card__value">info@viaexport.store</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Telegram -->
|
||||||
|
<a class="contact-card" href="https://t.me/DexarSupport_bot" target="_blank" rel="noopener">
|
||||||
|
<div class="contact-card__icon">
|
||||||
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="#2b9fd0"><path d="M9.04 15.65l-.36 4.06c.51 0 .73-.22.99-.48l2.38-2.27 4.93 3.6c.9.5 1.55.24 1.79-.83l3.24-15.18h.01c.29-1.34-.48-1.86-1.36-1.54L1.13 9.66c-1.32.5-1.3 1.23-.22 1.56l4.92 1.53L17.27 5.6c.54-.34 1.03-.15.62.19"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="contact-card__body">
|
||||||
|
<span class="contact-card__label">{{ 'contacts.tg_label' | translate }}</span>
|
||||||
|
<span class="contact-card__value">@DexarSupport_bot</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="info-section">
|
||||||
|
<h2 class="info-section__title">{{ 'contacts.hours_title' | translate }}</h2>
|
||||||
|
<div class="hours-table">
|
||||||
|
<div class="hours-row">
|
||||||
|
<span class="hours-row__label">{{ 'footer.support_label' | translate }}</span>
|
||||||
|
<span class="hours-row__value hours-row__value--green">24/7</span>
|
||||||
|
</div>
|
||||||
|
<div class="hours-row">
|
||||||
|
<span class="hours-row__label">{{ 'footer.questions_label' | translate }}</span>
|
||||||
|
<span class="hours-row__value">10:00–19:00 МСК</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
146
src/app/pages/contacts-page/contacts-page.scss
Normal file
146
src/app/pages/contacts-page/contacts-page.scss
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/app/pages/contacts-page/contacts-page.ts
Normal file
10
src/app/pages/contacts-page/contacts-page.ts
Normal file
@@ -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 {}
|
||||||
@@ -30,6 +30,9 @@
|
|||||||
<button type="button" class="method method--disabled" disabled aria-label="WeChat Pay">
|
<button type="button" class="method method--disabled" disabled aria-label="WeChat Pay">
|
||||||
<img class="method__logo" src="/wechat-pay.svg" alt="WeChat Pay" />
|
<img class="method__logo" src="/wechat-pay.svg" alt="WeChat Pay" />
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="method method--disabled" disabled aria-label="Alipay">
|
||||||
|
<img class="method__logo" src="/alipay.svg" alt="Alipay" />
|
||||||
|
</button>
|
||||||
<button type="button" class="method method--disabled" disabled aria-label="Visa">
|
<button type="button" class="method method--disabled" disabled aria-label="Visa">
|
||||||
<img class="method__logo" src="/visa.svg" alt="Visa" />
|
<img class="method__logo" src="/visa.svg" alt="Visa" />
|
||||||
</button>
|
</button>
|
||||||
@@ -77,13 +80,15 @@
|
|||||||
class="input-wrap__input"
|
class="input-wrap__input"
|
||||||
[ngModel]="amount()"
|
[ngModel]="amount()"
|
||||||
(ngModelChange)="onAmountChange($event)"
|
(ngModelChange)="onAmountChange($event)"
|
||||||
min="1"
|
[min]="minAmount()"
|
||||||
|
[max]="maxAmount()"
|
||||||
step="1"
|
step="1"
|
||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
autofocus
|
autofocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="field__hint">{{ 'create.amount_hint' | translate }} {{ minAmount() }}–{{ maxAmount().toLocaleString('ru') }} ₽</span>
|
||||||
@if (error()) {
|
@if (error()) {
|
||||||
<span class="field__error">{{ error() }}</span>
|
<span class="field__error">{{ error() }}</span>
|
||||||
}
|
}
|
||||||
@@ -102,7 +107,7 @@
|
|||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="pay-btn" type="button" (click)="createCheck()" [disabled]="loading()">
|
<button class="pay-btn" type="button" (click)="createCheck()" [disabled]="loading() || qrImageUrl() !== null">
|
||||||
<span class="pay-btn__icon">
|
<span class="pay-btn__icon">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
@@ -116,6 +121,17 @@
|
|||||||
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
|
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- QR section: shown after QR is created -->
|
||||||
|
@if (qrImageUrl()) {
|
||||||
|
<div class="qr-section">
|
||||||
|
<p class="qr-section__label">{{ 'create.qr_label' | translate }}</p>
|
||||||
|
<img class="qr-section__img" [src]="qrImageUrl()!" width="240" height="240" alt="QR" />
|
||||||
|
@if (qrPolling()) {
|
||||||
|
<p class="qr-section__hint">{{ 'create.qr_waiting' | translate }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card__footer">
|
<div class="card__footer">
|
||||||
|
|||||||
@@ -173,3 +173,36 @@
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── QR section ─────────────────────────────────────────────────────────────
|
||||||
|
.qr-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px 0 8px;
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #475569;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__img {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__hint {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #64748b;
|
||||||
|
animation: pulse 1.6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.45; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,8 +3,32 @@ import { FormsModule } from '@angular/forms';
|
|||||||
import { Router, RouterLink } from '@angular/router';
|
import { Router, RouterLink } from '@angular/router';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { FastcheckService } from '../../fastcheck.service';
|
import { FastcheckService } from '../../fastcheck.service';
|
||||||
import { FASTCHECK_API } from '../../api';
|
import { FASTCHECK_API, QR_VITANOVA_API } from '../../api';
|
||||||
import { TranslatePipe } from '../../translate/translate.pipe';
|
import { TranslatePipe } from '../../translate/translate.pipe';
|
||||||
|
import { TranslationService } from '../../translate/translation.service';
|
||||||
|
|
||||||
|
type PaymentMethod = 'sbp';
|
||||||
|
type Currency = 'RUB';
|
||||||
|
|
||||||
|
interface SettingsResponse {
|
||||||
|
minAmount?: number;
|
||||||
|
maxAmount?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateQrResponse {
|
||||||
|
qrId?: string;
|
||||||
|
payload?: string; // raw QR data string to encode
|
||||||
|
qrUrl?: string; // pre-rendered image URL (if provided)
|
||||||
|
Status?: boolean;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QrStatusResponse {
|
||||||
|
qrStatus?: string; // e.g. "PAID"
|
||||||
|
Status?: boolean;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
interface CreateFastcheckResponse {
|
interface CreateFastcheckResponse {
|
||||||
fastcheck: string;
|
fastcheck: string;
|
||||||
@@ -13,8 +37,13 @@ interface CreateFastcheckResponse {
|
|||||||
Status: boolean;
|
Status: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaymentMethod = 'sbp' | 'wechat' | 'visa' | 'master';
|
/** Generate a v4-like UUID without crypto dependency. */
|
||||||
type Currency = 'RUB' | 'CNY' | 'USD' | 'EUR' | 'AMD';
|
function generateUUID(): string {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||||
|
const r = (Math.random() * 16) | 0;
|
||||||
|
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-create-page',
|
selector: 'app-create-page',
|
||||||
@@ -26,19 +55,22 @@ export class CreatePage {
|
|||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
private store = inject(FastcheckService);
|
private store = inject(FastcheckService);
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
|
private i18n = inject(TranslationService);
|
||||||
|
|
||||||
amount = signal<number>(10);
|
private t(key: string): string { return this.i18n.translate(key); }
|
||||||
|
|
||||||
|
// Limits – updated from settings API on init.
|
||||||
|
minAmount = signal<number>(30);
|
||||||
|
maxAmount = signal<number>(200_000);
|
||||||
|
|
||||||
|
amount = signal<number>(100);
|
||||||
note = signal<string>('');
|
note = signal<string>('');
|
||||||
error = signal<string>('');
|
error = signal<string>('');
|
||||||
loading = signal<boolean>(false);
|
loading = signal<boolean>(false);
|
||||||
|
settingsLoaded = signal<boolean>(false);
|
||||||
|
|
||||||
payment = signal<PaymentMethod>('sbp');
|
|
||||||
currency = signal<Currency>('RUB');
|
currency = signal<Currency>('RUB');
|
||||||
|
payment = signal<PaymentMethod>('sbp');
|
||||||
/** sessionID for the Authorization header. Comes from ?session=... or websession. */
|
|
||||||
private get sessionId(): string {
|
|
||||||
return new URLSearchParams(window.location.search).get('session') ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
selectPayment(method: PaymentMethod, enabled: boolean): void {
|
selectPayment(method: PaymentMethod, enabled: boolean): void {
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
@@ -50,10 +82,49 @@ export class CreatePage {
|
|||||||
this.currency.set(c);
|
this.currency.set(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QR display state
|
||||||
|
qrImageUrl = signal<string | null>(null);
|
||||||
|
qrPolling = signal<boolean>(false);
|
||||||
|
private pollHandle: ReturnType<typeof setInterval> | null = null;
|
||||||
|
private activeQrId = '';
|
||||||
|
|
||||||
|
/** Auth credentials passed by the host page as URL params. */
|
||||||
|
private get authKey(): string {
|
||||||
|
return new URLSearchParams(window.location.search).get('auth-key') ?? '';
|
||||||
|
}
|
||||||
|
private get userId(): string {
|
||||||
|
return new URLSearchParams(window.location.search).get('user-id') ?? '';
|
||||||
|
}
|
||||||
|
private get sessionId(): string {
|
||||||
|
return new URLSearchParams(window.location.search).get('session') ?? '';
|
||||||
|
}
|
||||||
|
private get reference(): string {
|
||||||
|
return new URLSearchParams(window.location.search).get('ref') ?? window.location.hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadSettings(): void {
|
||||||
|
this.http.get<SettingsResponse>(`${QR_VITANOVA_API}/settings`).subscribe({
|
||||||
|
next: (s) => {
|
||||||
|
if (typeof s?.minAmount === 'number') this.minAmount.set(s.minAmount);
|
||||||
|
if (typeof s?.maxAmount === 'number') this.maxAmount.set(s.maxAmount);
|
||||||
|
this.settingsLoaded.set(true);
|
||||||
|
},
|
||||||
|
error: () => this.settingsLoaded.set(true) // proceed with defaults
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
createCheck(): void {
|
createCheck(): void {
|
||||||
const val = this.amount();
|
const val = this.amount();
|
||||||
if (!val || val <= 0) {
|
if (!val || val < this.minAmount()) {
|
||||||
this.error.set('Введите корректную сумму');
|
this.error.set(`${this.t('errors.invalid_amount')} (мин. ${this.minAmount()} ₽)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (val > this.maxAmount()) {
|
||||||
|
this.error.set(`${this.t('errors.invalid_amount')} (макс. ${this.maxAmount().toLocaleString('ru')} ₽)`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,35 +132,97 @@ export class CreatePage {
|
|||||||
this.loading.set(true);
|
this.loading.set(true);
|
||||||
|
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
if (this.sessionId) {
|
if (this.authKey) headers['authorization-key'] = this.authKey;
|
||||||
headers['Authorization'] = JSON.stringify({ sessionID: this.sessionId });
|
if (this.userId) headers['userid-value'] = this.userId;
|
||||||
}
|
|
||||||
|
const partnerqrID = generateUUID();
|
||||||
|
|
||||||
this.http
|
this.http
|
||||||
.post<CreateFastcheckResponse>(
|
.post<CreateQrResponse>(
|
||||||
`${FASTCHECK_API}/fastcheck`,
|
`${QR_VITANOVA_API}/qr`,
|
||||||
{ amount: val, currency: this.currency() },
|
{
|
||||||
|
qrtype: 'QRDynamic',
|
||||||
|
amount: val,
|
||||||
|
currency: this.currency(),
|
||||||
|
partnerqrID,
|
||||||
|
qrDescription: this.note().trim(),
|
||||||
|
Userid: this.userId,
|
||||||
|
Reference: this.reference
|
||||||
|
},
|
||||||
{ headers }
|
{ headers }
|
||||||
)
|
)
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (res) => {
|
next: (res) => {
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
|
if (res?.qrId || res?.payload || res?.qrUrl) {
|
||||||
|
this.activeQrId = res.qrId ?? '';
|
||||||
|
// Use server-provided image URL or generate one from payload string.
|
||||||
|
const qrData = res.qrUrl ?? (res.payload
|
||||||
|
? `https://api.qrserver.com/v1/create-qr-code/?size=240x240&margin=8&data=${encodeURIComponent(res.payload)}`
|
||||||
|
: null);
|
||||||
|
this.qrImageUrl.set(qrData);
|
||||||
|
if (this.activeQrId) this.startPolling(this.activeQrId);
|
||||||
|
} else {
|
||||||
|
this.error.set(this.t('errors.payment_failed'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.loading.set(false);
|
||||||
|
const msg: string | undefined = err?.error?.message;
|
||||||
|
this.error.set(msg ?? this.t('errors.lookup_failed'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private startPolling(qrId: string): void {
|
||||||
|
this.stopPolling();
|
||||||
|
this.qrPolling.set(true);
|
||||||
|
this.pollHandle = setInterval(() => {
|
||||||
|
this.http.get<QrStatusResponse>(`${QR_VITANOVA_API}/qr/dynamic/${qrId}`)
|
||||||
|
.subscribe({
|
||||||
|
next: (res) => {
|
||||||
|
const paid = res?.qrStatus === 'PAID' || res?.Status === true;
|
||||||
|
if (paid) {
|
||||||
|
this.stopPolling();
|
||||||
|
this.createFastcheck();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: () => undefined
|
||||||
|
});
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopPolling(): void {
|
||||||
|
if (this.pollHandle !== null) {
|
||||||
|
clearInterval(this.pollHandle);
|
||||||
|
this.pollHandle = null;
|
||||||
|
}
|
||||||
|
this.qrPolling.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createFastcheck(): void {
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
if (this.sessionId) headers['Authorization'] = JSON.stringify({ sessionID: this.sessionId });
|
||||||
|
|
||||||
|
this.http
|
||||||
|
.post<CreateFastcheckResponse>(
|
||||||
|
`${FASTCHECK_API}/fastcheck`,
|
||||||
|
{ amount: this.amount(), currency: this.currency() },
|
||||||
|
{ headers }
|
||||||
|
)
|
||||||
|
.subscribe({
|
||||||
|
next: (res) => {
|
||||||
if (res?.fastcheck) {
|
if (res?.fastcheck) {
|
||||||
this.store.setCreated({
|
this.store.setCreated({
|
||||||
fastcheck: res.fastcheck,
|
fastcheck: res.fastcheck,
|
||||||
code: res.code,
|
code: res.code,
|
||||||
amount: val,
|
amount: this.amount(),
|
||||||
expiration: res.expiration
|
expiration: res.expiration
|
||||||
});
|
});
|
||||||
|
}
|
||||||
this.router.navigate(['/']);
|
this.router.navigate(['/']);
|
||||||
} else {
|
|
||||||
this.error.set('Не удалось создать платёж.');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => this.router.navigate(['/'])
|
||||||
this.loading.set(false);
|
|
||||||
this.error.set('Ошибка при создании платежа. Попробуйте ещё раз.');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
[placeholder]="'fastcheck.number_placeholder' | translate"
|
[placeholder]="'fastcheck.number_placeholder' | translate"
|
||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
maxlength="14"
|
maxlength="20"
|
||||||
/>
|
/>
|
||||||
<a class="btn btn--ghost" routerLink="/new" aria-label="Создать новый fastCHECK">{{ 'fastcheck.number_new' | translate }}</a>
|
<a class="btn btn--ghost" routerLink="/new" aria-label="Создать новый fastCHECK">{{ 'fastcheck.number_new' | translate }}</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
step="1"
|
step="1"
|
||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
[readonly]="amountLoading() || fastcheckAmount() !== null"
|
[disabled]="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@if (amountLoading()) {
|
@if (amountLoading()) {
|
||||||
@@ -55,6 +55,28 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Share row — shown once amount is known -->
|
||||||
|
@if (fastcheckAmount() !== null && !amountLoading()) {
|
||||||
|
<div class="share-row">
|
||||||
|
<button type="button" class="share-btn share-btn--email" (click)="shareByEmail()"
|
||||||
|
[title]="'fastcheck.share_email' | translate">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="2" y="4" width="20" height="16" rx="2"/>
|
||||||
|
<path d="M2 7l10 7 10-7"/>
|
||||||
|
</svg>
|
||||||
|
{{ 'fastcheck.share_email' | translate }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="share-btn share-btn--tg" (click)="shareByTelegram()"
|
||||||
|
[title]="'fastcheck.share_tg' | translate">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M9.04 15.65l-.36 4.06c.51 0 .73-.22.99-.48l2.38-2.27 4.93 3.6c.9.5 1.55.24 1.79-.83l3.24-15.18h.01c.29-1.34-.48-1.86-1.36-1.54L1.13 9.66c-1.32.5-1.3 1.23-.22 1.56l4.92 1.53L17.27 5.6c.54-.34 1.03-.15.62.19"/>
|
||||||
|
</svg>
|
||||||
|
{{ 'fastcheck.share_tg' | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Code -->
|
<!-- Code -->
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="field__label" for="fcCode">{{ 'fastcheck.code_label' | translate }}</label>
|
<label class="field__label" for="fcCode">{{ 'fastcheck.code_label' | translate }}</label>
|
||||||
@@ -66,8 +88,9 @@
|
|||||||
(ngModelChange)="onCodeChange($event)"
|
(ngModelChange)="onCodeChange($event)"
|
||||||
[placeholder]="'fastcheck.code_placeholder' | translate"
|
[placeholder]="'fastcheck.code_placeholder' | translate"
|
||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
maxlength="5"
|
maxlength="6"
|
||||||
autocomplete="one-time-code"
|
autocomplete="one-time-code"
|
||||||
|
[disabled]="!codeEnabled()"
|
||||||
/>
|
/>
|
||||||
@if (error()) {
|
@if (error()) {
|
||||||
<span class="field__error">{{ error() }}</span>
|
<span class="field__error">{{ error() }}</span>
|
||||||
|
|||||||
@@ -8,6 +8,40 @@
|
|||||||
.input { flex: 1; min-width: 0; }
|
.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 {
|
.btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { HttpClient } from '@angular/common/http';
|
|||||||
import { FastcheckService } from '../../fastcheck.service';
|
import { FastcheckService } from '../../fastcheck.service';
|
||||||
import { FASTCHECK_API } from '../../api';
|
import { FASTCHECK_API } from '../../api';
|
||||||
import { TranslatePipe } from '../../translate/translate.pipe';
|
import { TranslatePipe } from '../../translate/translate.pipe';
|
||||||
|
import { TranslationService } from '../../translate/translation.service';
|
||||||
|
|
||||||
interface WebSessionResponse {
|
interface WebSessionResponse {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@@ -19,6 +20,7 @@ interface CheckFastcheckResponse {
|
|||||||
amount?: number;
|
amount?: number;
|
||||||
expiration: string;
|
expiration: string;
|
||||||
Status: boolean;
|
Status: boolean;
|
||||||
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -31,6 +33,9 @@ export class FastcheckPage {
|
|||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
private store = inject(FastcheckService);
|
private store = inject(FastcheckService);
|
||||||
private router = inject(Router);
|
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.
|
// Telegram bot used for the sign-in deep link.
|
||||||
private readonly telegramBot = 'DexarSupport_bot';
|
private readonly telegramBot = 'DexarSupport_bot';
|
||||||
@@ -38,6 +43,7 @@ export class FastcheckPage {
|
|||||||
fastcheckNumber = signal<string>('');
|
fastcheckNumber = signal<string>('');
|
||||||
fastcheckAmount = signal<number | null>(null);
|
fastcheckAmount = signal<number | null>(null);
|
||||||
fastcheckCode = signal<string>('');
|
fastcheckCode = signal<string>('');
|
||||||
|
codeEnabled = signal<boolean>(false);
|
||||||
error = signal<string>('');
|
error = signal<string>('');
|
||||||
amountLoading = signal<boolean>(false);
|
amountLoading = signal<boolean>(false);
|
||||||
|
|
||||||
@@ -52,7 +58,8 @@ export class FastcheckPage {
|
|||||||
canPay = computed(() => {
|
canPay = computed(() => {
|
||||||
const digits = this.fastcheckNumber().replace(/\D/g, '');
|
const digits = this.fastcheckNumber().replace(/\D/g, '');
|
||||||
const codeDigits = this.fastcheckCode().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(() => {
|
telegramLink = computed(() => {
|
||||||
@@ -74,6 +81,18 @@ export class FastcheckPage {
|
|||||||
this.fastcheckNumber.set(created.fastcheck);
|
this.fastcheckNumber.set(created.fastcheck);
|
||||||
this.fastcheckAmount.set(created.amount);
|
this.fastcheckAmount.set(created.amount);
|
||||||
this.fastcheckCode.set(created.code);
|
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: () => {
|
error: () => {
|
||||||
this.popupLoading.set(false);
|
this.popupLoading.set(false);
|
||||||
this.popupError.set('Не удалось создать сессию. Попробуйте ещё раз.');
|
this.popupError.set(this.t('errors.session_failed'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -154,12 +173,15 @@ export class FastcheckPage {
|
|||||||
next: () => {
|
next: () => {
|
||||||
this.popupLoading.set(false);
|
this.popupLoading.set(false);
|
||||||
this.paid.set(true);
|
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();
|
this.fireMerchantCallback();
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.popupLoading.set(false);
|
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);
|
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 {
|
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[] = [];
|
const groups: string[] = [];
|
||||||
for (let i = 0; i < digits.length; i += 4) {
|
for (let i = 0; i < digits.length; i += 6) {
|
||||||
groups.push(digits.slice(i, i + 4));
|
groups.push(digits.slice(i, i + 6));
|
||||||
}
|
}
|
||||||
const masked = groups.join('-');
|
const masked = groups.join('-');
|
||||||
this.fastcheckNumber.set(masked);
|
this.fastcheckNumber.set(masked);
|
||||||
this.error.set('');
|
this.error.set('');
|
||||||
|
|
||||||
// If number became incomplete, drop the previously fetched amount so the
|
if (digits.length < 18 && this.lastLookedUpNumber) {
|
||||||
// user doesn't see a stale value tied to a different (now-edited) number.
|
|
||||||
if (digits.length < 12 && this.lastLookedUpNumber) {
|
|
||||||
this.fastcheckAmount.set(null);
|
this.fastcheckAmount.set(null);
|
||||||
|
this.codeEnabled.set(false);
|
||||||
this.lastLookedUpNumber = '';
|
this.lastLookedUpNumber = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-lookup when 12 digits are entered (and we haven't already looked it up).
|
if (digits.length === 18 && masked !== this.lastLookedUpNumber) {
|
||||||
if (digits.length === 12 && masked !== this.lastLookedUpNumber) {
|
|
||||||
this.lookupFastcheck(masked);
|
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 {
|
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.fastcheckCode.set(digits);
|
||||||
this.error.set('');
|
this.error.set('');
|
||||||
}
|
}
|
||||||
@@ -215,28 +235,42 @@ export class FastcheckPage {
|
|||||||
this.lastLookedUpNumber = number;
|
this.lastLookedUpNumber = number;
|
||||||
this.amountLoading.set(true);
|
this.amountLoading.set(true);
|
||||||
this.fastcheckAmount.set(null);
|
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
|
this.http
|
||||||
.request<CheckFastcheckResponse>('GET', `${FASTCHECK_API}/fastcheck`, {
|
.get<CheckFastcheckResponse>(`${FASTCHECK_API}/fastcheck/${encodeURIComponent(number)}`)
|
||||||
body: { fastcheck: number },
|
|
||||||
params: { fastcheck: number }
|
|
||||||
})
|
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (res) => {
|
next: (res) => {
|
||||||
this.amountLoading.set(false);
|
this.amountLoading.set(false);
|
||||||
if (res?.Status && typeof res.amount === 'number') {
|
if (res?.Status && typeof res.amount === 'number') {
|
||||||
this.fastcheckAmount.set(res.amount);
|
this.fastcheckAmount.set(res.amount);
|
||||||
} else if (res?.Status === false) {
|
this.codeEnabled.set(true);
|
||||||
this.error.set('Платёж не найден или просрочен.');
|
} else {
|
||||||
|
this.error.set(res?.message ?? this.t('errors.not_found'));
|
||||||
|
this.lastLookedUpNumber = '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: () => {
|
error: (err) => {
|
||||||
this.amountLoading.set(false);
|
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 = '';
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { FormsModule } from '@angular/forms';
|
|||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { TranslatePipe } from '../../translate/translate.pipe';
|
import { TranslatePipe } from '../../translate/translate.pipe';
|
||||||
|
import { TranslationService } from '../../translate/translation.service';
|
||||||
|
|
||||||
interface LegacyPayResponse {
|
interface LegacyPayResponse {
|
||||||
payload?: string;
|
payload?: string;
|
||||||
@@ -25,6 +26,9 @@ interface LegacyPayResponse {
|
|||||||
export class LegacyPayPage {
|
export class LegacyPayPage {
|
||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
private route = inject(ActivatedRoute);
|
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';
|
private readonly LEGACY_API = 'https://qr.vitanova.network:567/qr';
|
||||||
|
|
||||||
@@ -57,9 +61,9 @@ export class LegacyPayPage {
|
|||||||
pay(): void {
|
pay(): void {
|
||||||
if (!this.canPay()) {
|
if (!this.canPay()) {
|
||||||
if (!this.paymentId()) {
|
if (!this.paymentId()) {
|
||||||
this.error.set('Не указан идентификатор платежа (параметр id)');
|
this.error.set(this.t('errors.not_found'));
|
||||||
} else {
|
} else {
|
||||||
this.error.set('Введите корректную сумму');
|
this.error.set(this.t('errors.invalid_amount'));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -80,12 +84,12 @@ export class LegacyPayPage {
|
|||||||
if (res?.payload) {
|
if (res?.payload) {
|
||||||
window.location.href = res.payload;
|
window.location.href = res.payload;
|
||||||
} else {
|
} else {
|
||||||
this.error.set('Сервер не вернул ссылку для оплаты.');
|
this.error.set(this.t('errors.payment_failed'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
this.error.set('Ошибка при создании платежа. Попробуйте ещё раз.');
|
this.error.set(this.t('errors.lookup_failed'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
26
src/app/pages/partners-page/partners-page.html
Normal file
26
src/app/pages/partners-page/partners-page.html
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<div class="info-page">
|
||||||
|
<div class="info-page__hero">
|
||||||
|
<h1 class="info-page__title">{{ 'partners.title' | translate }}</h1>
|
||||||
|
<p class="info-page__lead">{{ 'partners.lead' | translate }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="partners-grid">
|
||||||
|
@for (p of partners; track p.name) {
|
||||||
|
<div class="partner-card">
|
||||||
|
<div class="partner-card__logo">{{ p.logo }}</div>
|
||||||
|
<div class="partner-card__body">
|
||||||
|
<span class="partner-card__cat">{{ p.category | translate }}</span>
|
||||||
|
<h3 class="partner-card__name">{{ p.name }}</h3>
|
||||||
|
<p class="partner-card__city">📍 {{ p.city }}</p>
|
||||||
|
<p class="partner-card__desc">{{ p.desc | translate }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="partners-cta">
|
||||||
|
<h2 class="partners-cta__title">{{ 'partners.cta_title' | translate }}</h2>
|
||||||
|
<p class="partners-cta__text">{{ 'partners.cta_text' | translate }}</p>
|
||||||
|
<a class="partners-cta__btn" routerLink="/contacts">{{ 'partners.cta_btn' | translate }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
146
src/app/pages/partners-page/partners-page.scss
Normal file
146
src/app/pages/partners-page/partners-page.scss
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/app/pages/partners-page/partners-page.ts
Normal file
26
src/app/pages/partners-page/partners-page.ts
Normal file
@@ -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' },
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -11,16 +11,40 @@
|
|||||||
|
|
||||||
<!-- Desktop nav -->
|
<!-- Desktop nav -->
|
||||||
<nav class="site-header__nav" [attr.aria-label]="'header.aria_nav' | translate">
|
<nav class="site-header__nav" [attr.aria-label]="'header.aria_nav' | translate">
|
||||||
<a class="site-header__link" href="#about">{{ 'header.nav_about' | translate }}</a>
|
<a class="site-header__link" routerLink="/about">{{ 'header.nav_about' | translate }}</a>
|
||||||
<a class="site-header__link" href="#contacts">{{ 'header.nav_contacts' | translate }}</a>
|
<a class="site-header__link" routerLink="/partners">{{ 'header.nav_partners' | translate }}</a>
|
||||||
|
<a class="site-header__link" routerLink="/contacts">{{ 'header.nav_contacts' | translate }}</a>
|
||||||
<a class="site-header__link" href="mailto:info@viaexport.store">{{ 'header.nav_support' | translate }}</a>
|
<a class="site-header__link" href="mailto:info@viaexport.store">{{ 'header.nav_support' | translate }}</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Language switcher -->
|
<!-- Language dropdown -->
|
||||||
<div class="site-header__langs">
|
<div class="lang-select" [class.lang-select--open]="langOpen()">
|
||||||
<button type="button" class="site-header__lang" [class.site-header__lang--active]="currentLang() === 'ru'" (click)="setLang('ru')">RU</button>
|
<button type="button" class="lang-select__trigger" (click)="toggleLang()">
|
||||||
<button type="button" class="site-header__lang" [class.site-header__lang--active]="currentLang() === 'en'" (click)="setLang('en')">EN</button>
|
<img class="lang-select__flag" [src]="activeLang.flag" [alt]="activeLang.label" width="20" height="20" />
|
||||||
<button type="button" class="site-header__lang" [class.site-header__lang--active]="currentLang() === 'hy'" (click)="setLang('hy')">HY</button>
|
<span class="lang-select__code">{{ activeLang.code | uppercase }}</span>
|
||||||
|
<svg class="lang-select__chevron" width="12" height="12" viewBox="0 0 24 24" fill="none"
|
||||||
|
stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
||||||
|
<path d="M6 9l6 6 6-6"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
@if (langOpen()) {
|
||||||
|
<div class="lang-select__dropdown">
|
||||||
|
@for (lang of langs; track lang.code) {
|
||||||
|
<button type="button" class="lang-select__option"
|
||||||
|
[class.lang-select__option--active]="currentLang() === lang.code"
|
||||||
|
(click)="setLang(lang.code)">
|
||||||
|
<img class="lang-select__flag" [src]="lang.flag" [alt]="lang.label" width="20" height="20" />
|
||||||
|
<span class="lang-select__name">{{ lang.label }}</span>
|
||||||
|
@if (currentLang() === lang.code) {
|
||||||
|
<svg class="lang-select__check" width="14" height="14" viewBox="0 0 24 24" fill="none"
|
||||||
|
stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
||||||
|
<path d="M20 6L9 17l-5-5"/>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile hamburger -->
|
<!-- Mobile hamburger -->
|
||||||
@@ -42,17 +66,33 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile dropdown -->
|
<!-- Mobile overlay + drawer -->
|
||||||
@if (menuOpen()) {
|
@if (menuOpen()) {
|
||||||
<nav class="site-header__mobile-menu" (click)="closeMenu()" [attr.aria-label]="'header.aria_menu' | translate">
|
<div class="mobile-overlay" (click)="closeMenu()">
|
||||||
<a class="site-header__mobile-link" href="#about">{{ 'header.nav_about' | translate }}</a>
|
<nav class="mobile-panel" (click)="$event.stopPropagation()" [attr.aria-label]="'header.aria_menu' | translate">
|
||||||
<a class="site-header__mobile-link" href="#contacts">{{ 'header.nav_contacts' | translate }}</a>
|
<div class="mobile-panel__header">
|
||||||
<a class="site-header__mobile-link" href="mailto:info@viaexport.store">{{ 'header.nav_support' | translate }}</a>
|
<span class="mobile-panel__title">fastCHECK</span>
|
||||||
<div class="site-header__mobile-langs">
|
<button type="button" class="mobile-panel__close" (click)="closeMenu()" [attr.aria-label]="'header.aria_close' | translate">
|
||||||
<button type="button" class="site-header__lang" [class.site-header__lang--active]="currentLang() === 'ru'" (click)="setLang('ru')">RU</button>
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
||||||
<button type="button" class="site-header__lang" [class.site-header__lang--active]="currentLang() === 'en'" (click)="setLang('en')">EN</button>
|
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||||
<button type="button" class="site-header__lang" [class.site-header__lang--active]="currentLang() === 'hy'" (click)="setLang('hy')">HY</button>
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<a class="mobile-panel__link" routerLink="/about" (click)="closeMenu()">{{ 'header.nav_about' | translate }}</a>
|
||||||
|
<a class="mobile-panel__link" routerLink="/partners" (click)="closeMenu()">{{ 'header.nav_partners' | translate }}</a>
|
||||||
|
<a class="mobile-panel__link" routerLink="/contacts" (click)="closeMenu()">{{ 'header.nav_contacts' | translate }}</a>
|
||||||
|
<a class="mobile-panel__link" href="mailto:info@viaexport.store" (click)="closeMenu()">{{ 'header.nav_support' | translate }}</a>
|
||||||
|
<div class="mobile-panel__langs">
|
||||||
|
@for (lang of langs; track lang.code) {
|
||||||
|
<button type="button" class="site-header__lang"
|
||||||
|
[class.site-header__lang--active]="currentLang() === lang.code"
|
||||||
|
(click)="setLang(lang.code); closeMenu()">
|
||||||
|
<img [src]="lang.flag" [alt]="lang.label" width="20" height="20" />
|
||||||
|
<span>{{ lang.code | uppercase }}</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -71,19 +71,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__langs {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 2px;
|
|
||||||
margin-left: 8px;
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__lang {
|
&__lang {
|
||||||
padding: 5px 8px;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 5px 10px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -107,6 +99,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 8px 14px 4px;
|
padding: 8px 14px 4px;
|
||||||
|
border-top: 1px solid #f1f5f9;
|
||||||
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__burger {
|
&__burger {
|
||||||
@@ -132,25 +126,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__mobile-menu {
|
&__mobile-menu { display: none; } // replaced by .mobile-overlay / .mobile-panel
|
||||||
border-top: 1px solid #e2e8f0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 8px 12px 12px;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__mobile-link {
|
&__mobile-link { display: none; }
|
||||||
padding: 12px 14px;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #0f172a;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: background 0.15s;
|
|
||||||
|
|
||||||
&:hover { background: #f1f5f9; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wordmark colours
|
// Wordmark colours
|
||||||
@@ -167,3 +145,180 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.03em;
|
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); }
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 { RouterLink } from '@angular/router';
|
||||||
import { TranslatePipe } from '../translate/translate.pipe';
|
import { TranslatePipe } from '../translate/translate.pipe';
|
||||||
import { TranslationService, Lang } from '../translate/translation.service';
|
import { TranslationService, Lang } from '../translate/translation.service';
|
||||||
|
|
||||||
|
interface LangOption { code: Lang; label: string; flag: string; }
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-site-header',
|
selector: 'app-site-header',
|
||||||
imports: [RouterLink, TranslatePipe],
|
imports: [RouterLink, TranslatePipe, UpperCasePipe],
|
||||||
templateUrl: './site-header.html',
|
templateUrl: './site-header.html',
|
||||||
styleUrl: './site-header.scss'
|
styleUrl: './site-header.scss'
|
||||||
})
|
})
|
||||||
@@ -13,9 +16,33 @@ export class SiteHeader {
|
|||||||
private i18n = inject(TranslationService);
|
private i18n = inject(TranslationService);
|
||||||
|
|
||||||
menuOpen = signal(false);
|
menuOpen = signal(false);
|
||||||
|
langOpen = signal(false);
|
||||||
currentLang = this.i18n.currentLang;
|
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); }
|
toggleMenu(): void { this.menuOpen.update(v => !v); }
|
||||||
closeMenu(): void { this.menuOpen.set(false); }
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user