Compare commits
48 Commits
678ab3773b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a52fd07273 | |||
| fe7fea151a | |||
| e9acbd4898 | |||
| 4841cdf90d | |||
| c2a0675c79 | |||
| e62afe07eb | |||
| 926afc5691 | |||
| 45769ca817 | |||
| 02a33e9b14 | |||
| 9c96370235 | |||
| 9cbb6660f8 | |||
| b1ffd577c5 | |||
| bee56afedc | |||
| ce2c9c42fe | |||
| 17dfad5eaa | |||
| abb4f7b849 | |||
| 097064281a | |||
| 0330e0a212 | |||
| 5147d05ea2 | |||
| 14d9642568 | |||
| 6e7527cf1e | |||
| 957321ae1e | |||
| 11ea0793ba | |||
| ea291525e9 | |||
| 76e02e5ca6 | |||
| d37ca14f69 | |||
| e0df81c071 | |||
| 98423be0c3 | |||
| 1d19ddd47c | |||
| c0b7ac08fb | |||
| 889f289489 | |||
| be4e44d102 | |||
| 9fc7fbbae2 | |||
| f853a498ce | |||
| fb6e00c00b | |||
| 5eb7b2dcba | |||
| b4ad6da49e | |||
| 679e404dc8 | |||
| 138b774073 | |||
| b238282569 | |||
| 28a1d1be8b | |||
| 390367abda | |||
| 504e0c87fb | |||
| 0a86aaf4f6 | |||
| 59bda137e5 | |||
| cf634f766f | |||
| f97f4b5d96 | |||
| 629d03cdef |
4
.gitignore
vendored
@@ -5,8 +5,10 @@
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
/dist
|
||||
|
||||
# Local-only docs and scratch (not for publishing)
|
||||
/docs/
|
||||
changes.txt
|
||||
api.txt
|
||||
|
||||
# Node
|
||||
/node_modules
|
||||
|
||||
@@ -68,7 +68,8 @@
|
||||
"buildTarget": "qr_vitanova:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "qr_vitanova:build:development"
|
||||
"buildTarget": "qr_vitanova:build:development",
|
||||
"proxyConfig": "proxy.conf.json"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
|
||||
355
dist/3rdpartylicenses.txt
vendored
@@ -1,355 +0,0 @@
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/forms
|
||||
License: "MIT"
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2026 Google LLC. https://angular.dev/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/core
|
||||
License: "MIT"
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2026 Google LLC. https://angular.dev/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: rxjs
|
||||
License: "Apache-2.0"
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: tslib
|
||||
License: "0BSD"
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/common
|
||||
License: "MIT"
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2026 Google LLC. https://angular.dev/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/platform-browser
|
||||
License: "MIT"
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2026 Google LLC. https://angular.dev/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
Package: @angular/router
|
||||
License: "MIT"
|
||||
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2026 Google LLC. https://angular.dev/license
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
BIN
dist/favicon.ico
vendored
|
Before Width: | Height: | Size: 4.2 KiB |
17
dist/index.html
vendored
@@ -1,17 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="ru" data-beasties-container>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Оплата через СБП</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<meta name="theme-color" content="#2563eb">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<style>*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html,body{height:100%}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background:#1e40af}</style><link rel="stylesheet" href="styles-4STSJS4C.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles-4STSJS4C.css"></noscript></head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
<link rel="modulepreload" href="chunk-FBABAKVO.js"><script src="main-XKBOOIAP.js" type="module"></script></body>
|
||||
</html>
|
||||
3
dist/prerendered-routes.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"routes": {}
|
||||
}
|
||||
1
dist/styles-4STSJS4C.css
vendored
@@ -1 +0,0 @@
|
||||
*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html,body{height:100%}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background:#1e40af}
|
||||
@@ -148,7 +148,7 @@
|
||||
|
||||
<div class="card__header">
|
||||
<div class="sbp-logo">
|
||||
<img src="https://sbp.nspk.ru/storage/settings/common/logo/0645d335-8b62-43a1-9a33-0d4c9d1dc0e0.svg" alt="СБП" />
|
||||
<img src="public/sbp.svg" alt="СБП" />
|
||||
</div>
|
||||
<h1 class="card__title">Оплата через СБП</h1>
|
||||
<p class="card__subtitle">Система быстрых платежей</p>
|
||||
@@ -187,7 +187,7 @@
|
||||
id="note"
|
||||
class="note-input"
|
||||
placeholder="Причина платежа..."
|
||||
rows="3"
|
||||
rows="2"
|
||||
maxlength="500"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
23
proxy.conf.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"/proxy/legacy-qr": {
|
||||
"target": "https://qr.vitanova.network:567",
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"pathRewrite": { "^/proxy/legacy-qr": "" },
|
||||
"logLevel": "debug"
|
||||
},
|
||||
"/proxy/fastcheck": {
|
||||
"target": "https://api.fastcheck.store",
|
||||
"secure": true,
|
||||
"changeOrigin": true,
|
||||
"pathRewrite": { "^/proxy/fastcheck": "" },
|
||||
"logLevel": "debug"
|
||||
},
|
||||
"/proxy/qr-vitanova": {
|
||||
"target": "https://qr.vitanova.network",
|
||||
"secure": true,
|
||||
"changeOrigin": true,
|
||||
"pathRewrite": { "^/proxy/qr-vitanova": "" },
|
||||
"logLevel": "debug"
|
||||
}
|
||||
}
|
||||
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 |
26
public/example.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"nspkID": "AD10001UFDAT5FDD9LVPEJVDHN75UHHK",
|
||||
"amount": 100,
|
||||
"currency": "RUB",
|
||||
"order": "",
|
||||
"paymentDetails": "",
|
||||
"qrType": "QRDynamic",
|
||||
"qrExpirationDate": "",
|
||||
"qrExecutionDate": "",
|
||||
"sbpBank": "",
|
||||
"sbpMerchant": "",
|
||||
"sbpMerchantId": "",
|
||||
"sbpOperationId": 633730903,
|
||||
"status": "",
|
||||
"nspkurl": "https://qr.nspk.ru/AD10001UFDAT5FDD9LVPEJVDHN75UHHK",
|
||||
"statusurl": "https://pay.payworld.online/webapi/SbpPaymentStatus?id=3426680585\u0026sbpOperationId=633730903\u0026sector=24819\u0026signature=NjZkZjMyZDBjZTI1Zjc1NWUyZjYxNDRkM2ZjN2ViZWQ2YzcwYTc0NWM1ZWRlMGE1YzQyZWNlOTVjNWY1ODUxYg%3D%3D",
|
||||
"redirectUrl": "",
|
||||
"qrDescription": "Payment for shop 00897f30-414f-4b89-b1af-25596b15411c",
|
||||
"additionalInfo": "",
|
||||
"TTL": 0,
|
||||
"callbackUrl": "",
|
||||
"retry": 0,
|
||||
"partnerID": "",
|
||||
"partnerqrID": "00897f30-414f-4b89-b1af-25596b15411c",
|
||||
"requestIP": ""
|
||||
}
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
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
@@ -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
@@ -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 |
131
public/i18n/en.json
Normal file
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"header": {
|
||||
"nav_about": "About",
|
||||
"nav_contacts": "Contacts",
|
||||
"nav_partners": "Partners",
|
||||
"nav_support": "Support",
|
||||
"aria_nav": "Navigation",
|
||||
"aria_menu": "Mobile 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.",
|
||||
"contacts_heading": "Contacts",
|
||||
"russia": "Russia",
|
||||
"armenia": "Armenia",
|
||||
"support_label": "Tech support",
|
||||
"support_hours": "24/7",
|
||||
"questions_label": "Questions",
|
||||
"questions_hours": "10:00–19:00 MSK",
|
||||
"legal_heading": "Legal details",
|
||||
"legal_company": "LLC «VIAEXPORT»",
|
||||
"legal_inn_ru": "TIN (RU): 9909675800",
|
||||
"legal_inn_am": "TIN (AM): 01051049",
|
||||
"legal_kpp": "KPP: 770287001",
|
||||
"legal_ogrn": "OGRN: 282.110.1296681",
|
||||
"legal_address": "Armenia, 0201, Yerevan, Minskaya St. 21-23, apt. 44",
|
||||
"rights": "LLC «VIAEXPORT». All rights reserved.",
|
||||
"director": "Director: Amirkhanyan Sargis Artashesovich"
|
||||
},
|
||||
"fastcheck": {
|
||||
"subtitle": "Enter fastCHECK details or create a new one",
|
||||
"number_label": "fastCHECK number",
|
||||
"number_placeholder": "123456-123456-123456",
|
||||
"number_new": "New",
|
||||
"amount_label": "Amount",
|
||||
"amount_checking": "Checking…",
|
||||
"code_label": "Code",
|
||||
"code_placeholder": "000000",
|
||||
"pay_btn": "Pay",
|
||||
"modal_title": "Sign in via Telegram",
|
||||
"modal_sub": "Scan QR or open the link",
|
||||
"modal_loading": "Loading…",
|
||||
"modal_open_tg": "Open in Telegram",
|
||||
"modal_confirming": "Confirming payment…",
|
||||
"modal_waiting": "Waiting for sign-in…",
|
||||
"modal_paid_title": "Paid",
|
||||
"modal_paid_sub": "fastCHECK successfully accepted.",
|
||||
"share_email": "Send by email",
|
||||
"share_tg": "Send via Telegram"
|
||||
},
|
||||
"create": {
|
||||
"title": "New",
|
||||
"subtitle": "Enter the amount to top up",
|
||||
"back_label": "Back",
|
||||
"payment_label": "Payment method",
|
||||
"currency_label": "Currency",
|
||||
"amount_label": "Payment amount",
|
||||
"note_label": "Note",
|
||||
"note_placeholder": "Reason for payment...",
|
||||
"creating": "Creating…",
|
||||
"create_btn": "Create",
|
||||
"amount_hint": "Allowed amount:",
|
||||
"qr_label": "Scan QR to pay",
|
||||
"qr_waiting": "Waiting for payment confirmation…",
|
||||
"payment_done": "Payment completed successfully."
|
||||
},
|
||||
"sbp": {
|
||||
"title": "Pay via SBP",
|
||||
"subtitle": "Fast Payment System",
|
||||
"amount_label": "Payment amount",
|
||||
"currency_name": "Russian ruble",
|
||||
"note_label": "Note",
|
||||
"note_placeholder": "Reason for payment...",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
131
public/i18n/hy.json
Normal file
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"header": {
|
||||
"nav_about": "Ծառայության մասին",
|
||||
"nav_contacts": "Կապ",
|
||||
"nav_partners": "Գործընկերներ",
|
||||
"nav_support": "Աջակցություն",
|
||||
"aria_nav": "Նավիգացիա",
|
||||
"aria_menu": "Բջջային ընտրացանկ",
|
||||
"aria_burger": "Ընտրացանկ",
|
||||
"aria_close": "Փակել ընտրացանկը"
|
||||
},
|
||||
"footer": {
|
||||
"desc": "Ֆիզիկական անձանց համար վիրտուալ չեկերի նորարարական ծառայություն: Ստեղծեք թվային չեկեր առցանց և կանխիկացրեք դրանք գործընկեր բանկերի բանկոմատներում 24/7:",
|
||||
"contacts_heading": "Կապ",
|
||||
"russia": "Ռուսաստան",
|
||||
"armenia": "Հայաստան",
|
||||
"support_label": "Տեխ. աջակցություն",
|
||||
"support_hours": "24/7",
|
||||
"questions_label": "Հարցեր",
|
||||
"questions_hours": "10:00–19:00 MSK",
|
||||
"legal_heading": "Իրավաբանական տվյալներ",
|
||||
"legal_company": "ООО «ВИАЭКСПОРТ»",
|
||||
"legal_inn_ru": "ИНН (РФ): 9909675800",
|
||||
"legal_inn_am": "ИНН (AM): 01051049",
|
||||
"legal_kpp": "КПП: 770287001",
|
||||
"legal_ogrn": "ОГРН: 282.110.1296681",
|
||||
"legal_address": "Հայաստան, 0201, Երևան, Մինսկայա փ. 21-23, բն. 44",
|
||||
"rights": "ООО «ВИАЭКСПОРТ»: Բոլոր իրավունքները պաշտպանված են:",
|
||||
"director": "Տնօրեն՝ Ամիրխանյան Սարգիս Արտաշեսի"
|
||||
},
|
||||
"fastcheck": {
|
||||
"subtitle": "Մուտքագրեք fastCHECK տվյալները կամ ստեղծեք նորը",
|
||||
"number_label": "fastCHECK համար",
|
||||
"number_placeholder": "123456-123456-123456",
|
||||
"number_new": "Նոր",
|
||||
"amount_label": "Գումար",
|
||||
"amount_checking": "Ստուգվում է…",
|
||||
"code_label": "Կոդ",
|
||||
"code_placeholder": "000000",
|
||||
"pay_btn": "Վճարել",
|
||||
"modal_title": "Մուտք գործել Telegram-ով",
|
||||
"modal_sub": "Սկանավորեք QR կամ բացեք հղումը",
|
||||
"modal_loading": "Բեռնվում է…",
|
||||
"modal_open_tg": "Բացել Telegram-ում",
|
||||
"modal_confirming": "Վճարման հաստատում…",
|
||||
"modal_waiting": "Սպասում ենք մուտքի…",
|
||||
"modal_paid_title": "Վճարված է",
|
||||
"modal_paid_sub": "fastCHECK-ը հաջողությամբ ընդունված է:",
|
||||
"share_email": "Ուղարկել էլ. նամակով",
|
||||
"share_tg": "Ուղարկել Telegram-ով"
|
||||
},
|
||||
"create": {
|
||||
"title": "Նոր",
|
||||
"subtitle": "Նշեք համալրման գումարը",
|
||||
"back_label": "Հետ",
|
||||
"payment_label": "Վճարման եղանակ",
|
||||
"currency_label": "Արժույթ",
|
||||
"amount_label": "Վճարման գումար",
|
||||
"note_label": "Նշում",
|
||||
"note_placeholder": "Վճարման պատճառ...",
|
||||
"creating": "Ստեղծվում է…",
|
||||
"create_btn": "Ստեղծել",
|
||||
"amount_hint": "Թույլատրելի գումար՝",
|
||||
"qr_label": "Սկանավորեք QR-կոդը վճարելու համար",
|
||||
"qr_waiting": "Սպասում ենք վճարման հաստատման…",
|
||||
"payment_done": "Վճարումը հաջողությամբ ավարտվեց."
|
||||
},
|
||||
"sbp": {
|
||||
"title": "Վճարել SBP-ով",
|
||||
"subtitle": "Արագ վճարումների համակարգ",
|
||||
"amount_label": "Վճարման գումար",
|
||||
"currency_name": "Ռուսական ռուբլի",
|
||||
"note_label": "Նշում",
|
||||
"note_placeholder": "Վճարման պատճառ...",
|
||||
"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": "Կապվեք մեզ հետ"
|
||||
}
|
||||
}
|
||||
131
public/i18n/ru.json
Normal file
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"header": {
|
||||
"nav_about": "О сервисе",
|
||||
"nav_contacts": "Контакты",
|
||||
"nav_partners": "Партнёры",
|
||||
"nav_support": "Поддержка",
|
||||
"aria_nav": "Навигация",
|
||||
"aria_menu": "Мобильное меню",
|
||||
"aria_burger": "Меню",
|
||||
"aria_close": "Закрыть меню"
|
||||
},
|
||||
"footer": {
|
||||
"desc": "Инновационный сервис виртуальных чеков для физических лиц. Создавайте цифровые чеки онлайн и обналичивайте их через банкоматы банков-партнёров 24/7.",
|
||||
"contacts_heading": "Контакты",
|
||||
"russia": "Россия",
|
||||
"armenia": "Армения",
|
||||
"support_label": "Техподдержка",
|
||||
"support_hours": "24/7",
|
||||
"questions_label": "Вопросы",
|
||||
"questions_hours": "10:00–19:00 МСК",
|
||||
"legal_heading": "Реквизиты",
|
||||
"legal_company": "ООО «ВИАЭКСПОРТ»",
|
||||
"legal_inn_ru": "ИНН (РФ): 9909675800",
|
||||
"legal_inn_am": "ИНН (AM): 01051049",
|
||||
"legal_kpp": "КПП: 770287001",
|
||||
"legal_ogrn": "ОГРН: 282.110.1296681",
|
||||
"legal_address": "Армения, 0201, Ереван, ул. Минская, дом 21-23, кв. 44",
|
||||
"rights": "ООО «ВИАЭКСПОРТ». Все права защищены.",
|
||||
"director": "Директор: Амирханян Саргис Арташесович"
|
||||
},
|
||||
"fastcheck": {
|
||||
"subtitle": "Введите данные fastCHECK или создайте новый",
|
||||
"number_label": "Номер fastCHECK",
|
||||
"number_placeholder": "123456-123456-123456",
|
||||
"number_new": "Новый",
|
||||
"amount_label": "Сумма",
|
||||
"amount_checking": "Проверяем…",
|
||||
"code_label": "Код",
|
||||
"code_placeholder": "000000",
|
||||
"pay_btn": "Оплатить",
|
||||
"modal_title": "Войти через Telegram",
|
||||
"modal_sub": "Отсканируйте QR или откройте ссылку",
|
||||
"modal_loading": "Загрузка…",
|
||||
"modal_open_tg": "Открыть в Telegram",
|
||||
"modal_confirming": "Подтверждение оплаты…",
|
||||
"modal_waiting": "Ожидание входа…",
|
||||
"modal_paid_title": "Оплачено",
|
||||
"modal_paid_sub": "fastCHECK успешно принят.",
|
||||
"share_email": "Отправить на почту",
|
||||
"share_tg": "Отправить в Telegram"
|
||||
},
|
||||
"create": {
|
||||
"title": "Новый",
|
||||
"subtitle": "Укажите сумму для пополнения",
|
||||
"back_label": "Назад",
|
||||
"payment_label": "Способ оплаты",
|
||||
"currency_label": "Валюта",
|
||||
"amount_label": "Сумма платежа",
|
||||
"note_label": "Примечание",
|
||||
"note_placeholder": "Причина платежа...",
|
||||
"creating": "Создание…",
|
||||
"create_btn": "Создать",
|
||||
"amount_hint": "Допустимая сумма:",
|
||||
"qr_label": "Отсканируйте QR для оплаты",
|
||||
"qr_waiting": "Ожидаем подтверждения оплаты…",
|
||||
"payment_done": "Оплата успешно завершена."
|
||||
},
|
||||
"sbp": {
|
||||
"title": "Оплата через СБП",
|
||||
"subtitle": "Система быстрых платежей",
|
||||
"amount_label": "Сумма платежа",
|
||||
"currency_name": "Российский рубль",
|
||||
"note_label": "Примечание",
|
||||
"note_placeholder": "Причина платежа...",
|
||||
"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": "Связаться с нами"
|
||||
}
|
||||
}
|
||||
302
public/index (4).html
Normal file
@@ -0,0 +1,302 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Оплата через СБП</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<meta name="theme-color" content="#2563eb">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { height: 100%; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background: #1e40af;
|
||||
}
|
||||
|
||||
.page {
|
||||
min-height: 100dvh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #1e40af 0%, #2563eb 40%, #0ea5e9 100%);
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.page { align-items: flex-end; padding: 0; }
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 24px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,.18);
|
||||
overflow: hidden;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.card { border-radius: 24px 24px 0 0; max-width: 100%; box-shadow: 0 -8px 40px rgba(0,0,0,.15); }
|
||||
}
|
||||
|
||||
.card__header {
|
||||
background: linear-gradient(135deg, #1e40af 0%, #2563eb 100%);
|
||||
padding: 32px 28px 28px;
|
||||
text-align: center;
|
||||
}
|
||||
@media (max-width: 480px) { .card__header { padding: 28px 24px 24px; } }
|
||||
|
||||
.card__title { color: #fff; font-size: 22px; font-weight: 700; margin: 14px 0 4px; letter-spacing: -.3px; }
|
||||
.card__subtitle { color: rgba(255,255,255,.7); font-size: 13px; }
|
||||
|
||||
.card__body { padding: 28px 28px 20px; }
|
||||
@media (max-width: 480px) { .card__body { padding: 24px 20px 16px; } }
|
||||
|
||||
.card__footer { padding: 0 28px 24px; display: flex; justify-content: center; }
|
||||
@media (max-width: 480px) { .card__footer { padding: 0 20px 32px; } }
|
||||
|
||||
.sbp-logo {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
background: rgba(255,255,255,.15); backdrop-filter: blur(8px);
|
||||
border-radius: 16px; padding: 12px 20px;
|
||||
border: 1px solid rgba(255,255,255,.25);
|
||||
}
|
||||
.sbp-logo img { height: 40px; display: block; }
|
||||
@media (max-width: 480px) { .sbp-logo img { height: 34px; } }
|
||||
|
||||
.field { margin-bottom: 16px; }
|
||||
.field__label {
|
||||
display: block; font-size: 13px; font-weight: 600; color: #64748b;
|
||||
margin-bottom: 8px; text-transform: uppercase; letter-spacing: .6px;
|
||||
}
|
||||
.field__error { display: block; margin-top: 6px; font-size: 13px; color: #ef4444; font-weight: 500; }
|
||||
.field__error:empty { display: none; }
|
||||
|
||||
.input-wrap {
|
||||
display: flex; align-items: center;
|
||||
border: 2px solid #e2e8f0; border-radius: 14px;
|
||||
background: #f8fafc;
|
||||
transition: border-color .2s, box-shadow .2s, background .2s;
|
||||
}
|
||||
.input-wrap:focus-within {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 4px rgba(37,99,235,.12);
|
||||
background: #fff;
|
||||
}
|
||||
.input-wrap--error { border-color: #ef4444; box-shadow: 0 0 0 4px rgba(239,68,68,.1); }
|
||||
.input-wrap__prefix { padding: 0 4px 0 18px; font-size: 26px; font-weight: 700; color: #2563eb; user-select: none; line-height: 1; }
|
||||
.input-wrap__input {
|
||||
flex: 1; border: none; background: transparent;
|
||||
padding: 16px 16px 16px 8px; font-size: 32px; font-weight: 700;
|
||||
color: #0f172a; outline: none; min-width: 0; font-family: inherit;
|
||||
appearance: textfield; -moz-appearance: textfield;
|
||||
}
|
||||
.input-wrap__input::placeholder { color: #cbd5e1; }
|
||||
.input-wrap__input::-webkit-outer-spin-button,
|
||||
.input-wrap__input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
|
||||
@media (max-width: 480px) { .input-wrap__input { font-size: 28px; padding: 14px 14px 14px 6px; } }
|
||||
|
||||
.currency-badge {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
background: #f1f5f9; border-radius: 12px; padding: 12px 16px; margin-bottom: 20px;
|
||||
}
|
||||
.currency-badge__flag { font-size: 22px; line-height: 1; }
|
||||
.currency-badge__code { font-size: 15px; font-weight: 700; color: #0f172a; }
|
||||
.currency-badge__name { font-size: 13px; color: #64748b; margin-left: auto; }
|
||||
|
||||
.pay-btn {
|
||||
width: 100%; display: flex; align-items: center; justify-content: center;
|
||||
gap: 10px; padding: 17px 24px;
|
||||
background: linear-gradient(135deg, #1e40af 0%, #2563eb 100%);
|
||||
color: #fff; border: none; border-radius: 14px;
|
||||
font-size: 17px; font-weight: 700; letter-spacing: .2px;
|
||||
cursor: pointer; font-family: inherit;
|
||||
transition: opacity .15s, transform .1s, box-shadow .15s;
|
||||
box-shadow: 0 6px 20px rgba(37,99,235,.38);
|
||||
}
|
||||
.pay-btn:hover { opacity: .92; box-shadow: 0 8px 28px rgba(37,99,235,.45); }
|
||||
.pay-btn:active { transform: scale(.98); opacity: .88; }
|
||||
.pay-btn:disabled { opacity: .6; cursor: not-allowed; transform: none; }
|
||||
.pay-btn__icon { display: flex; align-items: center; }
|
||||
@media (max-width: 480px) { .pay-btn { padding: 16px 24px; font-size: 16px; } }
|
||||
|
||||
.secure-badge {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
font-size: 12px; color: #94a3b8; font-weight: 500;
|
||||
}
|
||||
|
||||
.note-input {
|
||||
width: 100%; border: 2px solid #e2e8f0; border-radius: 14px;
|
||||
background: #f8fafc; padding: 14px 16px;
|
||||
font-size: 15px; font-weight: 500; color: #0f172a;
|
||||
font-family: inherit; resize: vertical; outline: none;
|
||||
transition: border-color .2s, box-shadow .2s, background .2s;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.note-input::placeholder { color: #cbd5e1; font-weight: 400; }
|
||||
.note-input:focus {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 4px rgba(37,99,235,.12);
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="page">
|
||||
<div class="card">
|
||||
|
||||
<div class="card__header">
|
||||
<div class="sbp-logo">
|
||||
<img src="sbp.svg" alt="СБП" />
|
||||
</div>
|
||||
<h1 class="card__title">Оплата через СБП</h1>
|
||||
<p class="card__subtitle">Система быстрых платежей</p>
|
||||
</div>
|
||||
|
||||
<div class="card__body">
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="amount">Сумма платежа</label>
|
||||
<div class="input-wrap" id="inputWrap">
|
||||
<span class="input-wrap__prefix">₽</span>
|
||||
<input
|
||||
id="amount"
|
||||
type="number"
|
||||
class="input-wrap__input"
|
||||
min="1"
|
||||
step="1"
|
||||
inputmode="numeric"
|
||||
placeholder="0"
|
||||
autofocus
|
||||
value="50"
|
||||
/>
|
||||
</div>
|
||||
<span class="field__error" id="error"></span>
|
||||
</div>
|
||||
|
||||
<div class="currency-badge">
|
||||
<span class="currency-badge__flag">🇷🇺</span>
|
||||
<span class="currency-badge__code">RUB</span>
|
||||
<span class="currency-badge__name">Российский рубль</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="note">Примечание</label>
|
||||
<textarea
|
||||
id="note"
|
||||
class="note-input"
|
||||
placeholder="Причина платежа..."
|
||||
rows="3"
|
||||
maxlength="500"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button class="pay-btn" id="payBtn" onclick="goToPayment()">
|
||||
<span class="pay-btn__icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="1" y="4" width="22" height="16" rx="2" ry="2"/>
|
||||
<line x1="1" y1="10" x2="23" y2="10"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span id="btnText">Перейти к оплате</span>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="card__footer">
|
||||
<span class="secure-badge">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||
</svg>
|
||||
Защищённое соединение
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_URL = 'https://qr.vitanova.network:567/qr';
|
||||
|
||||
const amountInput = document.getElementById('amount');
|
||||
const noteInput = document.getElementById('note');
|
||||
const errorEl = document.getElementById('error');
|
||||
const payBtn = document.getElementById('payBtn');
|
||||
const btnText = document.getElementById('btnText');
|
||||
const inputWrap = document.getElementById('inputWrap');
|
||||
|
||||
amountInput.addEventListener('input', function () {
|
||||
if (Number(this.value) > 0) {
|
||||
errorEl.textContent = '';
|
||||
inputWrap.classList.remove('input-wrap--error');
|
||||
}
|
||||
});
|
||||
|
||||
function getPaymentId() {
|
||||
return new URLSearchParams(window.location.search).get('id');
|
||||
}
|
||||
|
||||
function setLoading(loading) {
|
||||
payBtn.disabled = loading;
|
||||
btnText.textContent = loading
|
||||
? 'Подождите...'
|
||||
: 'Перейти к оплате';
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
errorEl.textContent = msg;
|
||||
inputWrap.classList.add('input-wrap--error');
|
||||
}
|
||||
|
||||
async function goToPayment() {
|
||||
const amount = Number(amountInput.value);
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
showError('Введите корректную сумму');
|
||||
return;
|
||||
}
|
||||
|
||||
const id = getPaymentId();
|
||||
if (!id) {
|
||||
showError('Не указан идентификатор платежа (параметр id)');
|
||||
return;
|
||||
}
|
||||
|
||||
errorEl.textContent = '';
|
||||
inputWrap.classList.remove('input-wrap--error');
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch(API_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
qrtype: 'QRDynamic',
|
||||
amount: amount,
|
||||
currency: 'RUB',
|
||||
partnerqrID: id,
|
||||
qrDescription: noteInput.value.trim()
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.nspkurl) {
|
||||
window.location.href = data.nspkurl;
|
||||
} else {
|
||||
showError("nspkurl не найден в ответе сервера");
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showError("Ошибка запроса");
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 375 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 184 KiB |
1
public/sbp.svg
Normal file
|
After Width: | Height: | Size: 10 KiB |
@@ -1,8 +1,6 @@
|
||||
/**
|
||||
* Endpoint constants for the Fastcheck backend (see public/api.txt).
|
||||
* Centralised so they can be swapped in one place.
|
||||
*/
|
||||
export const FASTCHECK_API = 'https://api.fastcheck.store';
|
||||
import { isDevMode } from '@angular/core';
|
||||
|
||||
// Legacy QR endpoint kept for the SBP amount → payload redirect flow.
|
||||
export const QR_API = 'https://qr.vitanova.network:567/qr';
|
||||
// QR Vitanova API (dynamic QR, settings, polling).
|
||||
export const QR_VITANOVA_API = isDevMode()
|
||||
? '/proxy/qr-vitanova/api'
|
||||
: 'https://qr.vitanova.network/api';
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
<router-outlet />
|
||||
<!-- <app-site-header /> -->
|
||||
<main class="app-main">
|
||||
<router-outlet />
|
||||
</main>
|
||||
<!-- <app-site-footer /> -->
|
||||
@@ -3,19 +3,23 @@
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
loadComponent: () => {
|
||||
// Branch: ?id=<orderId> means legacy SBP merchant flow.
|
||||
const hasLegacyId = typeof window !== 'undefined'
|
||||
&& new URLSearchParams(window.location.search).has('id');
|
||||
return hasLegacyId
|
||||
? import('./pages/legacy-pay-page/legacy-pay-page').then((m) => m.LegacyPayPage)
|
||||
: import('./pages/fastcheck-page/fastcheck-page').then((m) => m.FastcheckPage);
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'new',
|
||||
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: '' }
|
||||
];
|
||||
|
||||
@@ -1,2 +1,12 @@
|
||||
:host { display: block; min-height: 100dvh; }
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
|
||||
export interface FastcheckData {
|
||||
fastcheck: string;
|
||||
amount: number;
|
||||
code: string;
|
||||
expiration?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared state between the home (Fastcheck) page and the create-new page.
|
||||
* When a new fastcheck is created via POST /fastcheck, the create page stores
|
||||
* the returned data here and the home page reads it to autofill its fields.
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FastcheckService {
|
||||
readonly created = signal<FastcheckData | null>(null);
|
||||
|
||||
setCreated(data: FastcheckData): void {
|
||||
this.created.set(data);
|
||||
}
|
||||
|
||||
consume(): FastcheckData | null {
|
||||
const value = this.created();
|
||||
this.created.set(null);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {}
|
||||
@@ -2,34 +2,28 @@
|
||||
<div class="card">
|
||||
|
||||
<div class="card__header">
|
||||
<a class="back" routerLink="/" aria-label="Назад">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M15 18l-6-6 6-6" />
|
||||
</svg>
|
||||
</a>
|
||||
<h1 class="card__title">
|
||||
Новый
|
||||
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
|
||||
</h1>
|
||||
<p class="card__subtitle">Укажите сумму для пополнения</p>
|
||||
<h1 class="card__title">Оплата через СБП</h1>
|
||||
<p class="card__subtitle">{{ 'create.subtitle' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<div class="card__body">
|
||||
|
||||
<!-- Payment methods -->
|
||||
<div class="field">
|
||||
<span class="field__label">Способ оплаты</span>
|
||||
<span class="field__label">{{ 'create.payment_label' | translate }}</span>
|
||||
<div class="methods">
|
||||
<button type="button" class="method" [class.method--active]="payment() === 'sbp'"
|
||||
(click)="selectPayment('sbp', true)" aria-label="СБП">
|
||||
<img class="method__logo"
|
||||
src="https://sbp.nspk.ru/storage/settings/common/logo/0645d335-8b62-43a1-9a33-0d4c9d1dc0e0.svg"
|
||||
src="/sbp.svg"
|
||||
alt="СБП" />
|
||||
</button>
|
||||
<button type="button" class="method method--disabled" disabled aria-label="WeChat Pay">
|
||||
<img class="method__logo" src="/wechat-pay.svg" alt="WeChat Pay" />
|
||||
</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">
|
||||
<img class="method__logo" src="/visa.svg" alt="Visa" />
|
||||
</button>
|
||||
@@ -41,31 +35,26 @@
|
||||
|
||||
<!-- Currencies -->
|
||||
<div class="field">
|
||||
<span class="field__label">Валюта</span>
|
||||
<span class="field__label">{{ 'create.currency_label' | translate }}</span>
|
||||
<div class="currencies">
|
||||
<button type="button" class="chip" [class.chip--active]="currency() === 'RUB'"
|
||||
(click)="selectCurrency('RUB', true)">
|
||||
<!-- <span class="chip__flag">🇷🇺</span> -->
|
||||
<span class="chip__sign">₽</span>
|
||||
<span class="chip__code">RUB</span>
|
||||
</button>
|
||||
<button type="button" class="chip chip--disabled" disabled>
|
||||
<!-- <span class="chip__flag">🇨🇳</span> -->
|
||||
<span class="chip__sign">¥</span>
|
||||
<span class="chip__code">CNY</span>
|
||||
</button>
|
||||
<button type="button" class="chip chip--disabled" disabled>
|
||||
<!-- <span class="chip__flag">🇺🇸</span> -->
|
||||
<span class="chip__sign">$</span>
|
||||
<span class="chip__code">USD</span>
|
||||
</button>
|
||||
<button type="button" class="chip chip--disabled" disabled>
|
||||
<!-- <span class="chip__flag">🇪🇺</span> -->
|
||||
<span class="chip__sign">€</span>
|
||||
<span class="chip__code">EUR</span>
|
||||
</button>
|
||||
<button type="button" class="chip chip--disabled" disabled>
|
||||
<!-- <span class="chip__flag">🇦🇲</span> -->
|
||||
<span class="chip__sign">֏</span>
|
||||
<span class="chip__code">AMD</span>
|
||||
</button>
|
||||
@@ -73,7 +62,7 @@
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="amount">Сумма платежа</label>
|
||||
<label class="field__label" for="amount">{{ 'create.amount_label' | translate }}</label>
|
||||
<div class="input-wrap" [class.input-wrap--error]="error()">
|
||||
<span class="input-wrap__prefix">₽</span>
|
||||
<input
|
||||
@@ -82,32 +71,34 @@
|
||||
class="input-wrap__input"
|
||||
[ngModel]="amount()"
|
||||
(ngModelChange)="onAmountChange($event)"
|
||||
min="1"
|
||||
[min]="minAmount()"
|
||||
[max]="maxAmount()"
|
||||
step="1"
|
||||
inputmode="numeric"
|
||||
placeholder="0"
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
<span class="field__hint">{{ 'create.amount_hint' | translate }} {{ minAmount() }}–{{ maxAmount().toLocaleString('ru') }} ₽</span>
|
||||
@if (error()) {
|
||||
<span class="field__error">{{ error() }}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="note">Примечание</label>
|
||||
<label class="field__label" for="note">{{ 'create.note_label' | translate }}</label>
|
||||
<textarea
|
||||
id="note"
|
||||
class="note-input"
|
||||
[ngModel]="note()"
|
||||
(ngModelChange)="onNoteChange($event)"
|
||||
placeholder="Причина платежа..."
|
||||
rows="3"
|
||||
[placeholder]="'create.note_placeholder' | translate"
|
||||
rows="2"
|
||||
maxlength="500"
|
||||
></textarea>
|
||||
</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">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -115,12 +106,36 @@
|
||||
</svg>
|
||||
</span>
|
||||
@if (loading()) {
|
||||
Создание…
|
||||
{{ 'create.creating' | translate }}
|
||||
} @else {
|
||||
Создать
|
||||
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
|
||||
{{ 'create.create_btn' | translate }}
|
||||
}
|
||||
</button>
|
||||
|
||||
<!-- QR popup overlay -->
|
||||
@if (qrImageUrl()) {
|
||||
<div class="qr-overlay" (click)="closeQr()">
|
||||
<div class="qr-modal" (click)="$event.stopPropagation()">
|
||||
<button class="qr-modal__close" type="button" (click)="closeQr()" aria-label="Close">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
<p class="qr-modal__label">{{ 'create.qr_label' | translate }}</p>
|
||||
<img class="qr-modal__img" [src]="qrImageUrl()!" width="260" height="260" alt="QR" />
|
||||
@if (qrStatus()) {
|
||||
<span class="qr-modal__status">{{ qrStatus() }}</span>
|
||||
}
|
||||
@if (qrPolling()) {
|
||||
<p class="qr-modal__hint">{{ 'create.qr_waiting' | translate }}</p>
|
||||
}
|
||||
@if (paymentDone()) {
|
||||
<p class="qr-modal__hint qr-modal__hint--done">{{ 'create.payment_done' | translate }}</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="card__footer">
|
||||
@@ -129,7 +144,7 @@
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||
</svg>
|
||||
Защищённое соединение
|
||||
{{ 'common.secure' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,15 +14,15 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
color: #475569;
|
||||
background: #f1f5f9;
|
||||
border: 1px solid #e2e8f0;
|
||||
text-decoration: none;
|
||||
transition: background 0.15s;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
z-index: 1;
|
||||
|
||||
&:hover { background: rgba(255, 255, 255, 0.28); }
|
||||
&:active { background: rgba(255, 255, 255, 0.36); }
|
||||
&:hover { background: #e2e8f0; color: #0f172a; }
|
||||
&:active { background: #cbd5e1; }
|
||||
}
|
||||
|
||||
.currency-badge {
|
||||
@@ -41,9 +41,15 @@
|
||||
|
||||
// ─── Methods row ────────────────────────────────────────────────────────────
|
||||
.methods {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
gap: 8px;
|
||||
padding-bottom: 4px;
|
||||
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
|
||||
@media (max-width: 360px) {
|
||||
gap: 6px;
|
||||
@@ -63,6 +69,8 @@
|
||||
transition: border-color .15s, background .15s, transform .1s, box-shadow .15s;
|
||||
-webkit-appearance: none;
|
||||
font-family: inherit;
|
||||
flex: 0 0 72px;
|
||||
width: 72px;
|
||||
|
||||
@media (max-width: 360px) {
|
||||
height: 52px;
|
||||
@@ -104,8 +112,14 @@
|
||||
// ─── Currency chips ─────────────────────────────────────────────────────────
|
||||
.currencies {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
gap: 8px;
|
||||
padding-bottom: 4px;
|
||||
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
}
|
||||
|
||||
.chip {
|
||||
@@ -173,3 +187,100 @@
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── QR section ─────────────────────────────────────────────────────────────
|
||||
// ─── QR popup ───────────────────────────────────────────────────────────────
|
||||
.qr-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: overlay-in 0.2s ease;
|
||||
}
|
||||
|
||||
.qr-modal {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 20px;
|
||||
padding: 32px 28px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.25);
|
||||
animation: modal-in 0.22s cubic-bezier(.34,1.56,.64,1);
|
||||
max-width: 340px;
|
||||
width: 90vw;
|
||||
|
||||
&__close {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.15s;
|
||||
&:hover { background: #e2e8f0; }
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 11px !important;
|
||||
font-weight: 500;
|
||||
color: #475569;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
margin: 0 auto 8px auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__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;
|
||||
}
|
||||
|
||||
&__status {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes overlay-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes modal-in {
|
||||
from { opacity: 0; transform: scale(0.85); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.45; }
|
||||
}
|
||||
|
||||
@@ -1,43 +1,69 @@
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { FastcheckService } from '../../fastcheck.service';
|
||||
import { FASTCHECK_API } from '../../api';
|
||||
import { QR_VITANOVA_API } from '../../api';
|
||||
import { TranslatePipe } from '../../translate/translate.pipe';
|
||||
import { TranslationService } from '../../translate/translation.service';
|
||||
|
||||
interface CreateFastcheckResponse {
|
||||
fastcheck: string;
|
||||
expiration: string;
|
||||
code: string;
|
||||
Status: boolean;
|
||||
type PaymentMethod = 'sbp';
|
||||
type Currency = 'RUB';
|
||||
|
||||
interface SettingsResponse {
|
||||
sbp?: boolean;
|
||||
wechat?: boolean;
|
||||
visa?: boolean;
|
||||
mastercard?: boolean;
|
||||
alipay?: boolean;
|
||||
rubles?: boolean;
|
||||
usd?: boolean;
|
||||
euro?: boolean;
|
||||
cny?: boolean;
|
||||
dram?: boolean;
|
||||
minAmount?: number;
|
||||
maxAmount?: number;
|
||||
qrTTL?: number;
|
||||
}
|
||||
|
||||
type PaymentMethod = 'sbp' | 'wechat' | 'visa' | 'master';
|
||||
type Currency = 'RUB' | 'CNY' | 'USD' | 'EUR' | 'AMD';
|
||||
interface CreateQrResponse {
|
||||
qrId?: string;
|
||||
nspkID?: string;
|
||||
Payload?: string; // per API doc (capital P)
|
||||
nspkurl?: string; // actual field name in real responses
|
||||
qrUrl?: string;
|
||||
status?: string; // e.g. "REGISTERED"
|
||||
}
|
||||
|
||||
interface QrStatusResponse {
|
||||
status?: string; // "REGISTERED" | "NEW" | "APPROVED" | "REJECTED" | "COMPLETED"
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-page',
|
||||
imports: [FormsModule, RouterLink],
|
||||
imports: [FormsModule, TranslatePipe],
|
||||
templateUrl: './create-page.html',
|
||||
styleUrl: './create-page.scss'
|
||||
})
|
||||
export class CreatePage {
|
||||
private http = inject(HttpClient);
|
||||
private store = inject(FastcheckService);
|
||||
private router = inject(Router);
|
||||
private i18n = inject(TranslationService);
|
||||
private readonly sites: Record<string, string> = {
|
||||
'51': 'fastcheck.store'
|
||||
};
|
||||
|
||||
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 | null>(null);
|
||||
note = signal<string>('');
|
||||
error = signal<string>('');
|
||||
loading = signal<boolean>(false);
|
||||
|
||||
payment = signal<PaymentMethod>('sbp');
|
||||
currency = signal<Currency>('RUB');
|
||||
|
||||
/** sessionID for the Authorization header. Comes from ?session=... or websession. */
|
||||
private get sessionId(): string {
|
||||
return new URLSearchParams(window.location.search).get('session') ?? '';
|
||||
}
|
||||
payment = signal<PaymentMethod>('sbp');
|
||||
|
||||
selectPayment(method: PaymentMethod, enabled: boolean): void {
|
||||
if (!enabled) return;
|
||||
@@ -49,10 +75,63 @@ export class CreatePage {
|
||||
this.currency.set(c);
|
||||
}
|
||||
|
||||
// QR display state
|
||||
qrImageUrl = signal<string | null>(null);
|
||||
qrPolling = signal<boolean>(false);
|
||||
qrStatus = signal<string>('');
|
||||
paymentDone = signal<boolean>(false);
|
||||
private pollHandle: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
/** Auth credentials passed by the host page as URL params. */
|
||||
private get authKey(): string {
|
||||
return new URLSearchParams(window.location.search).get('authorization-key') ?? '';
|
||||
}
|
||||
private get userId(): string {
|
||||
return new URLSearchParams(window.location.search).get('userid-value') ?? '';
|
||||
}
|
||||
private get reference(): string {
|
||||
return new URLSearchParams(window.location.search).get('ref') ?? window.location.hostname;
|
||||
}
|
||||
private get partnerqrID(): string {
|
||||
return new URLSearchParams(window.location.search).get('id') ?? '';
|
||||
}
|
||||
private get fromSite(): string {
|
||||
return new URLSearchParams(window.location.search).get('from') ?? '';
|
||||
}
|
||||
|
||||
get isMobile(): boolean {
|
||||
return window.innerWidth < 768;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.loadSettings();
|
||||
}
|
||||
|
||||
private loadSettings(): void {
|
||||
// Fetch limits from /qr/settings. If the call fails, keep defaults.
|
||||
const url = `${QR_VITANOVA_API}/qr/settings`;
|
||||
this.http.get<SettingsResponse>(url).subscribe({
|
||||
next: (s) => {
|
||||
if (typeof s?.minAmount === 'number') this.minAmount.set(s.minAmount);
|
||||
if (typeof s?.maxAmount === 'number') this.maxAmount.set(s.maxAmount);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createCheck(): void {
|
||||
const val = this.amount();
|
||||
if (!val || val <= 0) {
|
||||
this.error.set('Введите корректную сумму');
|
||||
if (val !== null && val < this.minAmount()) {
|
||||
this.error.set(`${this.t('errors.invalid_amount')} (мин. ${this.minAmount()} ₽)`);
|
||||
return;
|
||||
}
|
||||
if (val !== null && val > this.maxAmount()) {
|
||||
this.error.set(`${this.t('errors.invalid_amount')} (макс. ${this.maxAmount().toLocaleString('ru')} ₽)`);
|
||||
return;
|
||||
}
|
||||
|
||||
const partnerqrID = this.partnerqrID;
|
||||
if (!partnerqrID) {
|
||||
this.error.set(this.t('errors.lookup_failed'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -60,44 +139,144 @@ export class CreatePage {
|
||||
this.loading.set(true);
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (this.sessionId) {
|
||||
headers['Authorization'] = JSON.stringify({ sessionID: this.sessionId });
|
||||
}
|
||||
if (this.authKey) headers['authorization-key'] = this.authKey;
|
||||
if (this.userId) headers['userid-value'] = this.userId;
|
||||
|
||||
this.http
|
||||
.post<CreateFastcheckResponse>(
|
||||
`${FASTCHECK_API}/fastcheck`,
|
||||
{ amount: val, currency: this.currency() },
|
||||
.post<CreateQrResponse>(
|
||||
`${QR_VITANOVA_API}/qr`,
|
||||
{
|
||||
qrtype: 'QRDynamic',
|
||||
...(val !== null ? { amount: val } : {}),
|
||||
currency: this.currency(),
|
||||
partnerqrID,
|
||||
qrDescription: this.note().trim(),
|
||||
Userid: this.userId,
|
||||
Reference: this.reference,
|
||||
RedirectUrl: `https://fastcheck.store?id=fast-c202-4062-bcfb-8b4c8cc59adc`
|
||||
},
|
||||
{ headers }
|
||||
)
|
||||
.subscribe({
|
||||
next: (res) => {
|
||||
this.loading.set(false);
|
||||
if (res?.fastcheck) {
|
||||
this.store.setCreated({
|
||||
fastcheck: res.fastcheck,
|
||||
code: res.code,
|
||||
amount: val,
|
||||
expiration: res.expiration
|
||||
});
|
||||
this.router.navigate(['/']);
|
||||
const qrId = res?.qrId ?? res?.nspkID ?? '';
|
||||
// Real API uses 'nspkurl'; doc says 'Payload' — try both
|
||||
const nspkUrl = res?.nspkurl ?? res?.Payload;
|
||||
this.qrStatus.set(res?.status ?? '');
|
||||
|
||||
if (nspkUrl && this.isMobile) {
|
||||
window.location.href = nspkUrl;
|
||||
return;
|
||||
}
|
||||
|
||||
if (qrId || nspkUrl) {
|
||||
const qrData = nspkUrl
|
||||
? `https://api.qrserver.com/v1/create-qr-code/?size=256x256&margin=8&data=${encodeURIComponent(nspkUrl)}`
|
||||
: (res.qrUrl ?? null);
|
||||
this.qrImageUrl.set(qrData);
|
||||
if (qrId) this.startPolling(qrId);
|
||||
} else {
|
||||
this.error.set('Не удалось создать платёж.');
|
||||
this.error.set(this.t('errors.payment_failed'));
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
error: (err) => {
|
||||
this.loading.set(false);
|
||||
this.error.set('Ошибка при создании платежа. Попробуйте ещё раз.');
|
||||
const msg: string | undefined = err?.error?.message;
|
||||
this.error.set(msg ?? this.t('errors.lookup_failed'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onAmountChange(value: number): void {
|
||||
this.amount.set(value);
|
||||
if (value > 0) this.error.set('');
|
||||
private startPolling(qrId: string): void {
|
||||
this.stopPolling();
|
||||
this.qrPolling.set(true);
|
||||
this.pollHandle = setInterval(() => {
|
||||
this.http.get<QrStatusResponse>(`${QR_VITANOVA_API}/qr/dynamic/${encodeURIComponent(this.partnerqrID)}/${qrId}`)
|
||||
.subscribe({
|
||||
next: (res) => {
|
||||
const st = res?.status ?? '';
|
||||
this.qrStatus.set(st);
|
||||
if (st === 'COMPLETED' || st === 'APPROVED') {
|
||||
this.handlePaymentSuccess(res);
|
||||
} else if (st === 'REJECTED') {
|
||||
this.stopPolling();
|
||||
this.error.set(this.t('errors.payment_failed'));
|
||||
this.qrImageUrl.set(null);
|
||||
}
|
||||
// REGISTERED / NEW / '' — keep polling
|
||||
},
|
||||
error: () => {
|
||||
this.closeQr();
|
||||
this.error.set('оплата не прошла');
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
private stopPolling(): void {
|
||||
if (this.pollHandle !== null) {
|
||||
clearInterval(this.pollHandle);
|
||||
this.pollHandle = null;
|
||||
}
|
||||
this.qrPolling.set(false);
|
||||
}
|
||||
|
||||
onAmountChange(value: number | null): void {
|
||||
this.amount.set(value || null);
|
||||
if (value && value > 0) this.error.set('');
|
||||
}
|
||||
|
||||
onNoteChange(value: string): void {
|
||||
this.note.set(value);
|
||||
}
|
||||
|
||||
private handlePaymentSuccess(paidQr: QrStatusResponse): void {
|
||||
this.stopPolling();
|
||||
this.qrImageUrl.set(null);
|
||||
this.qrStatus.set('');
|
||||
this.paymentDone.set(true);
|
||||
|
||||
const id = this.partnerqrID;
|
||||
if (!id) {
|
||||
this.redirectToSource();
|
||||
return;
|
||||
}
|
||||
|
||||
this.http
|
||||
.post(`https://fastcheck.store/api/fastcheck/settings/${encodeURIComponent(id)}`, paidQr)
|
||||
.subscribe({
|
||||
next: () => this.redirectToSource(id),
|
||||
error: () => this.redirectToSource(id)
|
||||
});
|
||||
}
|
||||
|
||||
private redirectToSource(id?: string): void {
|
||||
const withId = (target: string): string => {
|
||||
if (!id) return target;
|
||||
|
||||
const normalizedTarget = /^https?:\/\//i.test(target) ? target : `https://${target}`;
|
||||
const url = new URL(normalizedTarget);
|
||||
url.searchParams.set('id', id);
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
const from = this.fromSite.trim();
|
||||
const target = this.sites[from];
|
||||
if (target) {
|
||||
window.location.href = withId(target);
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.history.length > 1) {
|
||||
window.history.back();
|
||||
}
|
||||
}
|
||||
|
||||
closeQr(): void {
|
||||
this.stopPolling();
|
||||
this.qrImageUrl.set(null);
|
||||
this.qrStatus.set('');
|
||||
this.paymentDone.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
<div class="page">
|
||||
<div class="card">
|
||||
|
||||
<div class="card__header">
|
||||
<img class="card__brand" src="/logo_big.png"
|
||||
alt="fastCHECK" width="220" height="60" />
|
||||
<p class="card__subtitle">
|
||||
Введите данные
|
||||
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
|
||||
или создайте новый
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card__body">
|
||||
|
||||
<!-- Fastcheck number + new -->
|
||||
<div class="field">
|
||||
<label class="field__label" for="fcNumber">
|
||||
Номер
|
||||
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
|
||||
</label>
|
||||
<div class="row">
|
||||
<input
|
||||
id="fcNumber"
|
||||
type="text"
|
||||
class="input"
|
||||
[ngModel]="fastcheckNumber()"
|
||||
(ngModelChange)="onNumberChange($event)"
|
||||
placeholder="1234-5678-0001"
|
||||
inputmode="numeric"
|
||||
autocomplete="off"
|
||||
maxlength="14"
|
||||
/>
|
||||
<a class="btn btn--ghost" routerLink="/new" aria-label="Создать новый fastCHECK">Новый</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Amount -->
|
||||
<div class="field">
|
||||
<label class="field__label" for="fcAmount">Сумма</label>
|
||||
<div class="input-wrap">
|
||||
<span class="input-wrap__prefix">₽</span>
|
||||
<input
|
||||
id="fcAmount"
|
||||
type="number"
|
||||
class="input-wrap__input"
|
||||
[ngModel]="fastcheckAmount()"
|
||||
(ngModelChange)="onAmountChange($event)"
|
||||
min="1"
|
||||
step="1"
|
||||
inputmode="numeric"
|
||||
placeholder="0"
|
||||
[readonly]="amountLoading() || fastcheckAmount() !== null"
|
||||
/>
|
||||
</div>
|
||||
@if (amountLoading()) {
|
||||
<span class="field__hint">Проверяем…</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Code -->
|
||||
<div class="field">
|
||||
<label class="field__label" for="fcCode">Код</label>
|
||||
<input
|
||||
id="fcCode"
|
||||
type="text"
|
||||
class="input"
|
||||
[ngModel]="fastcheckCode()"
|
||||
(ngModelChange)="onCodeChange($event)"
|
||||
placeholder="00000"
|
||||
inputmode="numeric"
|
||||
maxlength="5"
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
@if (error()) {
|
||||
<span class="field__error">{{ error() }}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button class="pay-btn" type="button" (click)="pay()" [disabled]="!canPay()">
|
||||
<span class="pay-btn__icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="1" y="4" width="22" height="16" rx="2" ry="2" />
|
||||
<line x1="1" y1="10" x2="23" y2="10" />
|
||||
</svg>
|
||||
</span>
|
||||
Оплатить
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card__footer">
|
||||
<span class="secure-badge">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||
</svg>
|
||||
Защищённое соединение
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Telegram sign-in popup -->
|
||||
@if (popupOpen()) {
|
||||
<div class="modal" (click)="closePopup()">
|
||||
<div class="modal__card" (click)="$event.stopPropagation()">
|
||||
<button class="modal__close" type="button" (click)="closePopup()" aria-label="Закрыть">×</button>
|
||||
|
||||
@if (paid()) {
|
||||
<div class="modal__success">
|
||||
<svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="#16a34a"
|
||||
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 6L9 17l-5-5" />
|
||||
</svg>
|
||||
<h2 class="modal__title">Оплачено</h2>
|
||||
<p class="modal__sub">
|
||||
<span class="brand"><span class="brand__fast">fast</span><span class="brand__check">CHECK</span></span>
|
||||
успешно принят.
|
||||
</p>
|
||||
</div>
|
||||
} @else {
|
||||
<img class="brand-logo brand-logo--small" src="/logo_small.png"
|
||||
alt="fastCHECK" width="32" height="32" />
|
||||
<h2 class="modal__title">Войти через Telegram</h2>
|
||||
<p class="modal__sub">Отсканируйте QR или откройте ссылку</p>
|
||||
|
||||
<div class="qr">
|
||||
@if (popupLoading() && !webSessionId()) {
|
||||
<div class="qr__placeholder">Загрузка…</div>
|
||||
} @else if (webSessionId()) {
|
||||
<img [src]="qrUrl()" width="240" height="240" alt="QR Telegram" />
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (webSessionId()) {
|
||||
<a class="tg-link" [href]="telegramLink()" target="_blank" rel="noopener">
|
||||
<svg width="18" height="18" 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>
|
||||
Открыть в Telegram
|
||||
</a>
|
||||
}
|
||||
|
||||
@if (popupLoading() && webSessionId()) {
|
||||
<p class="modal__hint">Подтверждение оплаты…</p>
|
||||
} @else if (webSessionId()) {
|
||||
<p class="modal__hint">Ожидание входа…</p>
|
||||
}
|
||||
|
||||
@if (popupError()) {
|
||||
<p class="modal__error">{{ popupError() }}</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
@use './../../../shared' as *;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: stretch;
|
||||
|
||||
.input { flex: 1; min-width: 0; }
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 16px;
|
||||
height: 48px;
|
||||
min-width: 64px;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
border: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
white-space: nowrap;
|
||||
transition: opacity .15s, transform .1s, background .15s;
|
||||
-webkit-appearance: none;
|
||||
|
||||
&--ghost {
|
||||
background: #f1f5f9;
|
||||
color: #2563eb;
|
||||
border-color: #e2e8f0;
|
||||
|
||||
&:hover { background: #e2e8f0; }
|
||||
&:active { transform: scale(.97); }
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
padding: 0 14px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color .2s, box-shadow .2s, background .2s;
|
||||
|
||||
&::placeholder { color: #cbd5e1; font-weight: 500; }
|
||||
|
||||
&:focus {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 4px rgba(37,99,235,.12);
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Modal (Telegram QR popup) ──────────────────────────────────────────────
|
||||
.modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
background: rgba(15, 23, 42, .55);
|
||||
backdrop-filter: blur(6px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
animation: fade-in .15s ease-out;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
align-items: stretch;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&__card {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 24px;
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
padding: 28px 24px 24px;
|
||||
text-align: center;
|
||||
box-shadow: 0 24px 60px rgba(0,0,0,.25);
|
||||
animation: pop-in .2s ease-out;
|
||||
margin: auto;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
max-width: 100%;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
padding: calc(28px + env(safe-area-inset-top)) 20px calc(28px + env(safe-area-inset-bottom));
|
||||
margin: 0;
|
||||
min-height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__close {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: background .15s;
|
||||
-webkit-appearance: none;
|
||||
|
||||
&:hover { background: #e2e8f0; }
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin: 4px 0 6px;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
margin: 0 0 18px;
|
||||
}
|
||||
|
||||
&__hint {
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
margin: 14px 0 0;
|
||||
}
|
||||
|
||||
&__error {
|
||||
font-size: 13px;
|
||||
color: #ef4444;
|
||||
font-weight: 500;
|
||||
margin: 12px 0 0;
|
||||
}
|
||||
|
||||
&__success {
|
||||
padding: 12px 0 4px;
|
||||
|
||||
svg { display: block; margin: 0 auto 10px; }
|
||||
}
|
||||
}
|
||||
|
||||
.qr {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8fafc;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
padding: 12px;
|
||||
width: 264px;
|
||||
height: 264px;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
@media (max-width: 380px) {
|
||||
width: min(264px, 70vw);
|
||||
height: auto;
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
color: #94a3b8;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 240px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.tg-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
padding: 14px 22px;
|
||||
min-height: 48px;
|
||||
border-radius: 12px;
|
||||
background: #229ED9;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
transition: opacity .15s;
|
||||
|
||||
&:hover { opacity: .9; }
|
||||
&:active { transform: scale(.97); }
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes pop-in {
|
||||
from { transform: translateY(12px) scale(.98); opacity: 0; }
|
||||
to { transform: translateY(0) scale(1); opacity: 1; }
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { FastcheckService } from '../../fastcheck.service';
|
||||
import { FASTCHECK_API } from '../../api';
|
||||
|
||||
interface WebSessionResponse {
|
||||
sessionId: string;
|
||||
userId: string;
|
||||
expires: string;
|
||||
userSessionId: string;
|
||||
Status: boolean;
|
||||
}
|
||||
|
||||
interface CheckFastcheckResponse {
|
||||
fastcheck: string;
|
||||
amount?: number;
|
||||
expiration: string;
|
||||
Status: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-fastcheck-page',
|
||||
imports: [FormsModule, RouterLink],
|
||||
templateUrl: './fastcheck-page.html',
|
||||
styleUrl: './fastcheck-page.scss'
|
||||
})
|
||||
export class FastcheckPage {
|
||||
private http = inject(HttpClient);
|
||||
private store = inject(FastcheckService);
|
||||
private router = inject(Router);
|
||||
|
||||
// Telegram bot used for the sign-in deep link.
|
||||
private readonly telegramBot = 'DexarSupport_bot';
|
||||
|
||||
fastcheckNumber = signal<string>('');
|
||||
fastcheckAmount = signal<number | null>(null);
|
||||
fastcheckCode = signal<string>('');
|
||||
error = signal<string>('');
|
||||
amountLoading = signal<boolean>(false);
|
||||
|
||||
popupOpen = signal<boolean>(false);
|
||||
popupLoading = signal<boolean>(false);
|
||||
popupError = signal<string>('');
|
||||
webSessionId = signal<string>('');
|
||||
paid = signal<boolean>(false);
|
||||
private pollHandle: ReturnType<typeof setInterval> | null = null;
|
||||
private lastLookedUpNumber = '';
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
telegramLink = computed(() => {
|
||||
const sid = this.webSessionId();
|
||||
return sid
|
||||
? `https://t.me/${this.telegramBot}?start=${encodeURIComponent(sid)}`
|
||||
: `https://t.me/${this.telegramBot}`;
|
||||
});
|
||||
|
||||
qrUrl = computed(() => {
|
||||
const link = this.telegramLink();
|
||||
return `https://api.qrserver.com/v1/create-qr-code/?size=240x240&margin=8&data=${encodeURIComponent(link)}`;
|
||||
});
|
||||
|
||||
constructor() {
|
||||
// Pull autofill data left over by the create page.
|
||||
const created = this.store.consume();
|
||||
if (created) {
|
||||
this.fastcheckNumber.set(created.fastcheck);
|
||||
this.fastcheckAmount.set(created.amount);
|
||||
this.fastcheckCode.set(created.code);
|
||||
}
|
||||
}
|
||||
|
||||
pay(): void {
|
||||
if (!this.canPay()) {
|
||||
return;
|
||||
}
|
||||
this.error.set('');
|
||||
this.openPopup();
|
||||
}
|
||||
|
||||
private openPopup(): void {
|
||||
this.popupOpen.set(true);
|
||||
this.popupError.set('');
|
||||
this.paid.set(false);
|
||||
this.popupLoading.set(true);
|
||||
|
||||
this.http.get<WebSessionResponse>(`${FASTCHECK_API}/websession`).subscribe({
|
||||
next: (res) => {
|
||||
this.popupLoading.set(false);
|
||||
this.webSessionId.set(res.sessionId);
|
||||
this.startPolling(res.sessionId);
|
||||
},
|
||||
error: () => {
|
||||
this.popupLoading.set(false);
|
||||
this.popupError.set('Не удалось создать сессию. Попробуйте ещё раз.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
closePopup(): void {
|
||||
this.popupOpen.set(false);
|
||||
this.stopPolling();
|
||||
if (this.webSessionId()) {
|
||||
// Best-effort logout; ignore errors.
|
||||
this.http
|
||||
.request('DELETE', `${FASTCHECK_API}/websession/${this.webSessionId()}`, {
|
||||
body: { sessionId: this.webSessionId() }
|
||||
})
|
||||
.subscribe({ error: () => undefined });
|
||||
}
|
||||
this.webSessionId.set('');
|
||||
}
|
||||
|
||||
private startPolling(sessionId: string): void {
|
||||
this.stopPolling();
|
||||
this.pollHandle = setInterval(() => {
|
||||
this.http
|
||||
.get<WebSessionResponse>(`${FASTCHECK_API}/websession/${sessionId}`)
|
||||
.subscribe({
|
||||
next: (res) => {
|
||||
if (res?.Status) {
|
||||
this.stopPolling();
|
||||
this.acceptFastcheck(sessionId);
|
||||
}
|
||||
},
|
||||
error: () => undefined
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
private stopPolling(): void {
|
||||
if (this.pollHandle !== null) {
|
||||
clearInterval(this.pollHandle);
|
||||
this.pollHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
private acceptFastcheck(sessionId: string): void {
|
||||
this.popupLoading.set(true);
|
||||
this.http
|
||||
.post(
|
||||
`${FASTCHECK_API}/fastcheck`,
|
||||
{ fastcheck: this.fastcheckNumber().trim(), code: this.fastcheckCode().trim() },
|
||||
{ headers: { Authorization: JSON.stringify({ sessionID: sessionId }) } }
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.popupLoading.set(false);
|
||||
this.paid.set(true);
|
||||
// Fire-and-forget merchant callback if a return_url is on the page.
|
||||
this.fireMerchantCallback();
|
||||
},
|
||||
error: () => {
|
||||
this.popupLoading.set(false);
|
||||
this.popupError.set('Не удалось принять платёж.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private fireMerchantCallback(): void {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const returnUrl = params.get('return_url');
|
||||
if (returnUrl) {
|
||||
setTimeout(() => {
|
||||
window.location.href = `${returnUrl}${returnUrl.includes('?') ? '&' : '?'}fastcheck=${encodeURIComponent(
|
||||
this.fastcheckNumber()
|
||||
)}&status=ok`;
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
onAmountChange(value: number | null): void {
|
||||
this.fastcheckAmount.set(value);
|
||||
}
|
||||
|
||||
/** Mask fastcheck number as XXXX-XXXX-XXXX, allow only digits. */
|
||||
onNumberChange(raw: string): void {
|
||||
const digits = (raw ?? '').replace(/\D/g, '').slice(0, 12);
|
||||
const groups: string[] = [];
|
||||
for (let i = 0; i < digits.length; i += 4) {
|
||||
groups.push(digits.slice(i, i + 4));
|
||||
}
|
||||
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) {
|
||||
this.fastcheckAmount.set(null);
|
||||
this.lastLookedUpNumber = '';
|
||||
}
|
||||
|
||||
// Auto-lookup when 12 digits are entered (and we haven't already looked it up).
|
||||
if (digits.length === 12 && masked !== this.lastLookedUpNumber) {
|
||||
this.lookupFastcheck(masked);
|
||||
}
|
||||
}
|
||||
|
||||
/** Allow only digits, max 5, in the code field. */
|
||||
onCodeChange(raw: string): void {
|
||||
const digits = (raw ?? '').replace(/\D/g, '').slice(0, 5);
|
||||
this.fastcheckCode.set(digits);
|
||||
this.error.set('');
|
||||
}
|
||||
|
||||
private lookupFastcheck(number: string): void {
|
||||
this.lastLookedUpNumber = number;
|
||||
this.amountLoading.set(true);
|
||||
this.fastcheckAmount.set(null);
|
||||
|
||||
// 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<CheckFastcheckResponse>('GET', `${FASTCHECK_API}/fastcheck`, {
|
||||
body: { fastcheck: number },
|
||||
params: { fastcheck: 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('Платёж не найден или просрочен.');
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.amountLoading.set(false);
|
||||
this.error.set('Не удалось проверить номер. Попробуйте ещё раз.');
|
||||
this.lastLookedUpNumber = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
<div class="page">
|
||||
<div class="card">
|
||||
|
||||
<div class="card__header">
|
||||
<div class="sbp-logo">
|
||||
<img src="https://sbp.nspk.ru/storage/settings/common/logo/0645d335-8b62-43a1-9a33-0d4c9d1dc0e0.svg"
|
||||
alt="СБП" />
|
||||
</div>
|
||||
<h1 class="card__title">Оплата через СБП</h1>
|
||||
<p class="card__subtitle">Система быстрых платежей</p>
|
||||
</div>
|
||||
|
||||
<div class="card__body">
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="amount">Сумма платежа</label>
|
||||
<div class="input-wrap" [class.input-wrap--error]="error()">
|
||||
<span class="input-wrap__prefix">₽</span>
|
||||
<input
|
||||
id="amount"
|
||||
type="number"
|
||||
class="input-wrap__input"
|
||||
[ngModel]="amount()"
|
||||
(ngModelChange)="onAmountChange($event)"
|
||||
min="1"
|
||||
step="1"
|
||||
inputmode="numeric"
|
||||
placeholder="0"
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
@if (error()) {
|
||||
<span class="field__error">{{ error() }}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="currency-badge">
|
||||
<span class="currency-badge__flag">🇷🇺</span>
|
||||
<span class="currency-badge__code">RUB</span>
|
||||
<span class="currency-badge__name">Российский рубль</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field__label" for="note">Примечание</label>
|
||||
<textarea
|
||||
id="note"
|
||||
class="note-input"
|
||||
[ngModel]="note()"
|
||||
(ngModelChange)="onNoteChange($event)"
|
||||
placeholder="Причина платежа..."
|
||||
rows="3"
|
||||
maxlength="500"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<button class="pay-btn" type="button" (click)="pay()" [disabled]="loading()">
|
||||
<span class="pay-btn__icon">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="1" y="4" width="22" height="16" rx="2" ry="2" />
|
||||
<line x1="1" y1="10" x2="23" y2="10" />
|
||||
</svg>
|
||||
</span>
|
||||
{{ loading() ? 'Подождите...' : 'Перейти к оплате' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card__footer">
|
||||
<span class="secure-badge">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||||
</svg>
|
||||
Защищённое соединение
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,60 +0,0 @@
|
||||
@use './../../../shared' as *;
|
||||
|
||||
.sbp-logo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
backdrop-filter: blur(8px);
|
||||
border-radius: 16px;
|
||||
padding: 12px 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
margin-bottom: 14px;
|
||||
|
||||
img {
|
||||
height: 40px;
|
||||
display: block;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
height: 34px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.currency-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: #f1f5f9;
|
||||
border-radius: 12px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 18px;
|
||||
|
||||
&__flag { font-size: 22px; line-height: 1; }
|
||||
&__code { font-size: 15px; font-weight: 700; color: #0f172a; }
|
||||
&__name { font-size: 13px; color: #64748b; margin-left: auto; }
|
||||
}
|
||||
|
||||
.note-input {
|
||||
width: 100%;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 14px;
|
||||
background: #f8fafc;
|
||||
padding: 14px 16px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #0f172a;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
outline: none;
|
||||
transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
|
||||
line-height: 1.5;
|
||||
|
||||
&::placeholder { color: #cbd5e1; font-weight: 400; }
|
||||
|
||||
&:focus {
|
||||
border-color: #2563eb;
|
||||
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.12);
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
interface LegacyPayResponse {
|
||||
payload?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy SBP merchant payment flow.
|
||||
* Activated when the root URL has `?id=<orderId>`.
|
||||
* Mirrors public/payment.html behaviour:
|
||||
* POST https://qr.vitanova.network:567/qr
|
||||
* { payment, amount, currency, id, note } -> { payload: '<sbp-deep-link>' }
|
||||
* then window.location.href = payload.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-legacy-pay-page',
|
||||
imports: [FormsModule],
|
||||
templateUrl: './legacy-pay-page.html',
|
||||
styleUrl: './legacy-pay-page.scss'
|
||||
})
|
||||
export class LegacyPayPage {
|
||||
private http = inject(HttpClient);
|
||||
private route = inject(ActivatedRoute);
|
||||
|
||||
private readonly LEGACY_API = 'https://qr.vitanova.network:567/qr';
|
||||
|
||||
amount = signal<number | null>(null);
|
||||
note = signal<string>('');
|
||||
error = signal<string>('');
|
||||
loading = signal<boolean>(false);
|
||||
|
||||
paymentId = signal<string>('');
|
||||
|
||||
canPay = computed(() => {
|
||||
const a = this.amount();
|
||||
return !!this.paymentId() && a !== null && a > 0 && !this.loading();
|
||||
});
|
||||
|
||||
constructor() {
|
||||
const id = this.route.snapshot.queryParamMap.get('id') ?? '';
|
||||
this.paymentId.set(id);
|
||||
}
|
||||
|
||||
onAmountChange(value: number | null): void {
|
||||
this.amount.set(value);
|
||||
if (this.error()) this.error.set('');
|
||||
}
|
||||
|
||||
onNoteChange(value: string): void {
|
||||
this.note.set(value);
|
||||
}
|
||||
|
||||
pay(): void {
|
||||
if (!this.canPay()) {
|
||||
if (!this.paymentId()) {
|
||||
this.error.set('Не указан идентификатор платежа (параметр id)');
|
||||
} else {
|
||||
this.error.set('Введите корректную сумму');
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.error.set('');
|
||||
this.loading.set(true);
|
||||
|
||||
const body = {
|
||||
payment: 'sbp',
|
||||
amount: this.amount(),
|
||||
currency: 'rub',
|
||||
id: this.paymentId(),
|
||||
note: this.note().trim()
|
||||
};
|
||||
|
||||
this.http.post<LegacyPayResponse>(this.LEGACY_API, body).subscribe({
|
||||
next: (res) => {
|
||||
this.loading.set(false);
|
||||
if (res?.payload) {
|
||||
window.location.href = res.payload;
|
||||
} else {
|
||||
this.error.set('Сервер не вернул ссылку для оплаты.');
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.loading.set(false);
|
||||
this.error.set('Ошибка при создании платежа. Попробуйте ещё раз.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
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
@@ -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
@@ -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' },
|
||||
];
|
||||
}
|
||||
57
src/app/site-footer/site-footer.html
Normal file
@@ -0,0 +1,57 @@
|
||||
<footer class="site-footer">
|
||||
<div class="site-footer__inner">
|
||||
|
||||
<!-- Brand + about -->
|
||||
<div class="site-footer__col site-footer__col--brand">
|
||||
<a class="site-footer__brand" href="/">
|
||||
<!-- <img src="/logo_big.png" alt="fastCHECK" width="28" height="28" /> -->
|
||||
<span class="site-footer__wordmark">
|
||||
<span class="wm-fast">fast</span><span class="wm-check">CHECK</span>
|
||||
</span>
|
||||
</a>
|
||||
<p class="site-footer__desc" id="about">{{ 'footer.desc' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Contacts -->
|
||||
<div class="site-footer__col" id="contacts">
|
||||
<h3 class="site-footer__heading">{{ 'footer.contacts_heading' | translate }}</h3>
|
||||
<ul class="site-footer__list">
|
||||
<li>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.07 10.5a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 21 16.92z"/></svg>
|
||||
<a href="tel:+79299037443">+7 (929) 903-74-43</a> <span class="site-footer__note">{{ 'footer.russia' | translate }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.07 10.5a19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 3 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 21 16.92z"/></svg>
|
||||
<a href="tel:+37498632421">+374 98 632421</a> <span class="site-footer__note">{{ 'footer.armenia' | translate }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>
|
||||
<a href="mailto:info@viaexport.store">info@viaexport.store</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="site-footer__hours">
|
||||
<p><strong>{{ 'footer.support_label' | translate }}:</strong> {{ 'footer.support_hours' | translate }}</p>
|
||||
<p><strong>{{ 'footer.questions_label' | translate }}:</strong> {{ 'footer.questions_hours' | translate }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Legal -->
|
||||
<div class="site-footer__col">
|
||||
<h3 class="site-footer__heading">{{ 'footer.legal_heading' | translate }}</h3>
|
||||
<ul class="site-footer__list site-footer__list--legal">
|
||||
<li>{{ 'footer.legal_company' | translate }}</li>
|
||||
<li>{{ 'footer.legal_inn_ru' | translate }}</li>
|
||||
<li>{{ 'footer.legal_inn_am' | translate }}</li>
|
||||
<li>{{ 'footer.legal_kpp' | translate }}</li>
|
||||
<li>{{ 'footer.legal_ogrn' | translate }}</li>
|
||||
<li class="site-footer__address">{{ 'footer.legal_address' | translate }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="site-footer__bottom">
|
||||
<p>© {{ year }} {{ 'footer.rights' | translate }}</p>
|
||||
<!-- <p>{{ 'footer.director' | translate }}</p> -->
|
||||
</div>
|
||||
</footer>
|
||||
156
src/app/site-footer/site-footer.scss
Normal file
@@ -0,0 +1,156 @@
|
||||
:host { display: block; }
|
||||
|
||||
.site-footer {
|
||||
background: #0f172a;
|
||||
color: #94a3b8;
|
||||
|
||||
&__inner {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px 32px;
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr 1fr;
|
||||
gap: 40px;
|
||||
|
||||
@media (max-width: 860px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 32px;
|
||||
padding: 36px 20px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
&__col {
|
||||
&--brand {
|
||||
@media (max-width: 860px) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-decoration: none;
|
||||
margin-bottom: 14px;
|
||||
|
||||
img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
object-fit: contain;
|
||||
filter: brightness(0) invert(1);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
&__wordmark {
|
||||
font-size: 18px;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
font-size: 13.5px;
|
||||
line-height: 1.65;
|
||||
color: #64748b;
|
||||
max-width: 380px;
|
||||
}
|
||||
|
||||
&__heading {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
color: #e2e8f0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 18px;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13.5px;
|
||||
|
||||
svg { flex-shrink: 0; opacity: 0.5; }
|
||||
}
|
||||
|
||||
a {
|
||||
color: #94a3b8;
|
||||
text-decoration: none;
|
||||
transition: color 0.15s;
|
||||
|
||||
&:hover { color: #e2e8f0; }
|
||||
}
|
||||
|
||||
&--legal {
|
||||
li {
|
||||
display: block;
|
||||
font-size: 12.5px;
|
||||
color: #64748b;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__note {
|
||||
font-size: 11px;
|
||||
color: #475569;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
&__hours {
|
||||
font-size: 12.5px;
|
||||
color: #64748b;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
&__address {
|
||||
color: #475569;
|
||||
font-size: 12px !important;
|
||||
line-height: 1.5;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
&__bottom {
|
||||
border-top: 1px solid #1e293b;
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 24px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 24px;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
|
||||
@media (max-width: 560px) {
|
||||
flex-direction: column;
|
||||
padding: 14px 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wm-fast {
|
||||
font-weight: 400;
|
||||
font-size: 0.72em;
|
||||
color: #64748b;
|
||||
margin-right: 0.04em;
|
||||
}
|
||||
.wm-check {
|
||||
font-weight: 700;
|
||||
font-size: 1em;
|
||||
color: #93c5fd;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
12
src/app/site-footer/site-footer.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { TranslatePipe } from '../translate/translate.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-site-footer',
|
||||
imports: [TranslatePipe],
|
||||
templateUrl: './site-footer.html',
|
||||
styleUrl: './site-footer.scss'
|
||||
})
|
||||
export class SiteFooter {
|
||||
year = new Date().getFullYear();
|
||||
}
|
||||
95
src/app/site-header/site-header.html
Normal file
@@ -0,0 +1,95 @@
|
||||
<header class="site-header">
|
||||
<div class="site-header__inner">
|
||||
|
||||
<!-- Brand -->
|
||||
<a class="site-header__brand" routerLink="/" (click)="closeMenu()">
|
||||
<img src="/logo_small.png" alt="SBP Pay" width="22" height="22" />
|
||||
</a>
|
||||
|
||||
<!-- Desktop nav -->
|
||||
<nav class="site-header__nav" [attr.aria-label]="'header.aria_nav' | translate">
|
||||
<a class="site-header__link" routerLink="/about">{{ 'header.nav_about' | 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>
|
||||
</nav>
|
||||
|
||||
<!-- Language dropdown -->
|
||||
<div class="lang-select" [class.lang-select--open]="langOpen()">
|
||||
<button type="button" class="lang-select__trigger" (click)="toggleLang()">
|
||||
<img class="lang-select__flag" [src]="activeLang.flag" [alt]="activeLang.label" width="20" height="20" />
|
||||
<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>
|
||||
|
||||
<!-- Mobile hamburger -->
|
||||
<button class="site-header__burger" type="button"
|
||||
[attr.aria-expanded]="menuOpen()"
|
||||
[attr.aria-label]="'header.aria_burger' | translate"
|
||||
(click)="toggleMenu()">
|
||||
@if (menuOpen()) {
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2.5" stroke-linecap="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
} @else {
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2.5" stroke-linecap="round">
|
||||
<line x1="3" y1="7" x2="21" y2="7"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="17" x2="21" y2="17"/>
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile overlay + drawer -->
|
||||
@if (menuOpen()) {
|
||||
<div class="mobile-overlay" (click)="closeMenu()">
|
||||
<nav class="mobile-panel" (click)="$event.stopPropagation()" [attr.aria-label]="'header.aria_menu' | translate">
|
||||
<div class="mobile-panel__header">
|
||||
<span class="mobile-panel__title">SBP Pay</span>
|
||||
<button type="button" class="mobile-panel__close" (click)="closeMenu()" [attr.aria-label]="'header.aria_close' | translate">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</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>
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
</header>
|
||||
324
src/app/site-header/site-header.scss
Normal file
@@ -0,0 +1,324 @@
|
||||
:host {
|
||||
display: block;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 900;
|
||||
}
|
||||
|
||||
.site-header {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
box-shadow: 0 1px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
&__inner {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&__brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-decoration: none;
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
&__wordmark {
|
||||
font-size: 18px;
|
||||
letter-spacing: -0.02em;
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__link {
|
||||
padding: 8px 14px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #475569;
|
||||
text-decoration: none;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #f1f5f9;
|
||||
color: #0f172a;
|
||||
}
|
||||
}
|
||||
|
||||
&__lang {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
font-family: inherit;
|
||||
|
||||
&:hover { background: #f1f5f9; color: #475569; }
|
||||
|
||||
&--active {
|
||||
background: #eff6ff;
|
||||
color: #1e40af;
|
||||
}
|
||||
}
|
||||
|
||||
&__mobile-langs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 8px 14px 4px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
&__burger {
|
||||
display: none;
|
||||
margin-left: auto;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.15s;
|
||||
-webkit-appearance: none;
|
||||
font-family: inherit;
|
||||
|
||||
&:hover { background: #f1f5f9; }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
&__mobile-menu { display: none; } // replaced by .mobile-overlay / .mobile-panel
|
||||
|
||||
&__mobile-link { display: none; }
|
||||
}
|
||||
|
||||
// Wordmark colours
|
||||
.wm-fast {
|
||||
font-weight: 400;
|
||||
font-size: 0.72em;
|
||||
color: #64748b;
|
||||
margin-right: 0.04em;
|
||||
}
|
||||
.wm-check {
|
||||
font-weight: 700;
|
||||
font-size: 1em;
|
||||
color: #1e40af;
|
||||
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); }
|
||||
}
|
||||
48
src/app/site-header/site-header.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
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, UpperCasePipe],
|
||||
templateUrl: './site-header.html',
|
||||
styleUrl: './site-header.scss'
|
||||
})
|
||||
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); }
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/app/translate/translate.pipe.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Pipe, PipeTransform, inject } from '@angular/core';
|
||||
import { TranslationService } from './translation.service';
|
||||
|
||||
@Pipe({ name: 'translate', pure: false, standalone: true })
|
||||
export class TranslatePipe implements PipeTransform {
|
||||
private svc = inject(TranslationService);
|
||||
|
||||
transform(key: string): string {
|
||||
return this.svc.translate(key);
|
||||
}
|
||||
}
|
||||
36
src/app/translate/translation.service.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Injectable, inject, signal } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
|
||||
export type Lang = 'ru' | 'en' | 'hy';
|
||||
type Translations = Record<string, Record<string, string>>;
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TranslationService {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
currentLang = signal<Lang>('ru');
|
||||
private translations = signal<Translations>({});
|
||||
|
||||
constructor() {
|
||||
this.load('ru');
|
||||
}
|
||||
|
||||
setLanguage(lang: Lang): void {
|
||||
this.currentLang.set(lang);
|
||||
this.load(lang);
|
||||
}
|
||||
|
||||
private load(lang: Lang): void {
|
||||
this.http.get<Translations>(`/i18n/${lang}.json`).subscribe({
|
||||
next: data => this.translations.set(data),
|
||||
});
|
||||
}
|
||||
|
||||
translate(key: string): string {
|
||||
const dot = key.indexOf('.');
|
||||
if (dot === -1) return key;
|
||||
const section = key.slice(0, dot);
|
||||
const k = key.slice(dot + 1);
|
||||
return this.translations()[section]?.[k] ?? key;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>fastCHECK</title>
|
||||
<title>QR Vitanova</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#2563eb">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Imported via @use './../../../shared' as *;
|
||||
|
||||
.page {
|
||||
min-height: 100dvh;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -29,15 +29,16 @@
|
||||
border-radius: 0;
|
||||
max-width: 100%;
|
||||
box-shadow: none;
|
||||
min-height: 100dvh;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__header {
|
||||
background: linear-gradient(135deg, #1e40af 0%, #2563eb 100%);
|
||||
padding: 28px 24px 24px;
|
||||
background: #ffffff;
|
||||
padding: 28px 24px 20px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
|
||||
@media (max-width: 480px) {
|
||||
padding-top: calc(28px + env(safe-area-inset-top));
|
||||
@@ -45,7 +46,7 @@
|
||||
}
|
||||
|
||||
&__title {
|
||||
color: #fff;
|
||||
color: #0f172a;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 4px;
|
||||
@@ -53,7 +54,7 @@
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
|
||||