mirror of
https://github.com/cerc-io/mars-interface.git
synced 2024-11-17 03:09:20 +00:00
v1.0.0
This commit is contained in:
parent
37309b46fb
commit
5f019f3fdd
12
.eslintrc.json
Normal file
12
.eslintrc.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals"],
|
||||
"plugins": ["simple-import-sort", "unused-imports"],
|
||||
"rules": {
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error",
|
||||
"react/display-name": "warn",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": "off"
|
||||
}
|
||||
}
|
26
.gitignore
vendored
26
.gitignore
vendored
@ -1,6 +1,5 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
@ -9,18 +8,33 @@
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
yarn.lock
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# Sentry
|
||||
.sentryclirc
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
|
12
.prettierrc
12
.prettierrc
@ -1,9 +1,7 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"bracketSpacing": true,
|
||||
"semi": false,
|
||||
"tabWidth": 4,
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "always"
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"semi": false,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
|
15
.vscode/launch.json
vendored
Normal file
15
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "pwa-chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:8080",
|
||||
"webRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.formatOnSave": true,
|
||||
"todo-tree.tree.scanMode": "workspace only"
|
||||
}
|
461
LICENSE
Normal file
461
LICENSE
Normal file
@ -0,0 +1,461 @@
|
||||
MARS PROTOCOL WEB APPLICATION LICENSE AGREEMENT
|
||||
Version 1, 24 January 2023
|
||||
|
||||
This Mars Protocol Web Application License Agreement (this “Agreement”)is
|
||||
a legally binding agreement with Delphi Labs Ltd., a British Virgin Islands
|
||||
company limited by shares (“Licensor”) pertaining to all software and
|
||||
technologies contained in this repository (whether in source code,
|
||||
object code or other form).
|
||||
|
||||
YOU ARE NOT PERMITTED TO USE THIS SOFTWARE EXCEPT FOR PURPOSES OF FACILITATING
|
||||
USE OF DEPLOYED INSTANCES OF THE PROTOCOL THAT ARE ENDORSED BY THE MARTIAN
|
||||
COUNCIL, UPON THE TERMS AND CONDITIONS SET FORTH BELOW.
|
||||
|
||||
YOU ARE NOT PERMITTED TO USE THIS SOFTWARE IN CONNECTION WITH FORKS OF
|
||||
THE PROTOCOL NOT ENDORSED BY THE MARTIAN COUNCIL, OR IN CONNECTION WITH
|
||||
OTHER PROTOCOLS.
|
||||
|
||||
1. RESERVATION OF PROPRIETARY RIGHTS.
|
||||
Except to the extent provided in Section 2:
|
||||
|
||||
a. all intellectual property (including all trade secrets, source
|
||||
code, designs and protocols) relating to the Web Application has
|
||||
been published or made available for informational purposes only
|
||||
(e.g., to enable users of the Web Application to conduct their
|
||||
own due diligence into the security and other risks thereof);
|
||||
|
||||
b. no license, right of reproduction or distribution or other right
|
||||
with respect to the Web Application or any other intellectual
|
||||
property is granted or implied; andc.all moral, intellectual
|
||||
property and other rights relating to the Web Application and
|
||||
other intellectual property are hereby reserved by Licensor
|
||||
(and the other contributors to such intellectual property or
|
||||
holders of such rights, as applicable).
|
||||
|
||||
2. LIMITED LICENSE.
|
||||
Upon the terms and subject to the conditions set forth in this Agreement
|
||||
(including the conditions set forth in Section 3), Licensor hereby grants
|
||||
a non-transferable, personal, non-sub-licensable, global, royalty-free,
|
||||
revocable license in Licensor’s intellectual property rights relating to
|
||||
the Web Application:
|
||||
|
||||
a. to each Authorized Site Operator, to run the Web Application for
|
||||
use by each Authorized User solely in connection with the Endorsed
|
||||
Smart Contracts (and not for any of the purposes described in
|
||||
ection 3);
|
||||
|
||||
b. to each Authorized User, to use the Web Application run by an
|
||||
Authorized Site Operatorsolely in connection with the Endorsed
|
||||
Smart Contracts (and not for any of the purposes described in
|
||||
Section 3); and
|
||||
|
||||
The following capitalized terms have the definitions that are ascribed
|
||||
to them below:
|
||||
|
||||
Defined Terms Relating to Relevant Persons
|
||||
|
||||
“Authorized Site Operator” means a person who makes the un-modified
|
||||
Web Application available to persons in good faith on commercially
|
||||
reasonable terms for purposes of facilitating their use of the
|
||||
Endorsed Smart Contracts for their intended purposes and complies
|
||||
with the conditions set forth in Section 3.
|
||||
|
||||
“Authorized User” means a person who uses the un-modified Web
|
||||
Application in good faith for purposes of using the Endorsed Smart
|
||||
Contracts for their intended purposes and complies with the conditions
|
||||
set forth in Section 3.
|
||||
|
||||
“Web Application” means the software at
|
||||
<https://github.com/mars-protocol/webapp>, as it may be updated from
|
||||
time to time by Licensor.
|
||||
|
||||
Defined Terms Relating to Mars Hub & Martian Council
|
||||
|
||||
“$MARS” means the native token unit of Mars Hub the bonding, staking
|
||||
or delegation of which determines which Mars Hub Core Nodes have the
|
||||
ability to propose and validate new blocks on the Mars Hub blockchain.
|
||||
|
||||
“Mars Hub” means, at each time, the canonical blockchain and virtual
|
||||
machine environment of the Mars Hub ‘mainnet’, as recognized by at
|
||||
least a majority of the Mars Core Nodes then being operated in good
|
||||
faith in the ordinary course of the network.
|
||||
|
||||
“Mars Hub Core” means the reference implementation of the Mars
|
||||
blockchain hub protocol currently stored at
|
||||
<https://github.com/mars-protocol/hub> or any successor thereto
|
||||
expressly determined by the Martian Council to constitute the
|
||||
reference implementation for Mars blockchain hub protocol.
|
||||
|
||||
“Mars Hub Core Nodes” means, at each time, the internet-connected
|
||||
computers then running unaltered and correctly configured instances
|
||||
of the most up-to-date production release of Mars Hub Core.
|
||||
|
||||
“Martian Council” means at each time, all persons holding $MARS that
|
||||
is staked with or delegated or bonded to Mars Hub Core Nodes in the
|
||||
active validator set for Mars Hub at such time and has the power to
|
||||
vote such $MARS tokens on governance proposals in accordance with
|
||||
the Mars Protocol.
|
||||
|
||||
Defined Terms Relating to Mars Protocol & Smart Contracts
|
||||
|
||||
“Endorsed Smart Contracts” means the Mainnet Smart Contracts and the
|
||||
Testnet Smart Contracts.
|
||||
|
||||
“Mainnet Smart Contracts” means all runtime object code that satisfies
|
||||
all of the following conditions precedent: (a) an instance of such code
|
||||
is deployed to a production-grade, commercial-grade “mainnet”
|
||||
blockchain-based network environment; (b) such code constitutes a part
|
||||
of the Mars Protocol; and (c) such instance of such code has been
|
||||
approved by the Martian Council to be governed by the Martian Council
|
||||
through Mars Hub on such blockchain-based network environment.
|
||||
|
||||
“Mars Protocol” means the software code at <https://github.com/mars-protocol>
|
||||
or any successor thereto expressly determined by the Martian Council to
|
||||
constitute or form a part of the “Mars Protocol”.
|
||||
|
||||
“Testnet Smart Contracts” means all runtime object code that satisfies
|
||||
all of the following conditions precedent: (a) an instance of such code
|
||||
is deployed to a nonproduction-grade, non-commercial-grade “testnet”
|
||||
blockchain-based network environment solely for testing purposes;
|
||||
(b) such code constitutes a part of the Mars Protocol; and (c) such
|
||||
deployment is in reasonable anticipation of, or follows, approval by
|
||||
the Martian Council of Mainnet Smart Contracts for the corresponding
|
||||
“mainnet” blockchain network environment.
|
||||
|
||||
3. CONDITIONS/PROHIBITED USES.
|
||||
Notwithstanding Section 2, it is a condition precedent and condition
|
||||
subsequent of the licenses granted hereunder that the Web Application must
|
||||
not be used in connection with or in furtherance of:
|
||||
|
||||
a. developing, making available, running or operating the Web
|
||||
Application for use by any person in connection with any smart
|
||||
contracts other than the Endorsed Smart Contracts;
|
||||
|
||||
b. any device, plan, scheme or artifice to defraud, or otherwise
|
||||
materially mislead, any person;
|
||||
|
||||
c. any fraud, deceit, material misrepresentation or other crime, tort
|
||||
or illegal conduct againstany person or device, plan, scheme or
|
||||
artifice for accomplishing the foregoing;
|
||||
|
||||
d. any violation, breach or failure to comply with any term or
|
||||
condition of this Agreement(including any inaccuracy in a
|
||||
epresentation of set forth in Section 4) or any other terms of
|
||||
service, privacy policy, trading policy or other contract
|
||||
governing the use of the Web Application or any Endorsed Smart
|
||||
Contract;
|
||||
|
||||
e. any fork, copy, derivative or alternative instance of any Endorsed
|
||||
Smart Contract;
|
||||
|
||||
f. any smart contract, platform or service that competes in any
|
||||
material respect with any Endorsed Smart Contract;
|
||||
|
||||
g. any device, plan, scheme or artifice to obtain any unfair
|
||||
competitive advantage over Licensor or other persons with an
|
||||
economic or beneficial interest in the Mainnet Smart Contracts;
|
||||
|
||||
h. any device, plan, scheme or artifice to interfere with, damage,
|
||||
impair or subvert the intended functioning of any Endorsed Smart
|
||||
Contract,including in connection with any “sybil attack”,
|
||||
“reentrancy attack”, “DoS attack,” “eclipse attack,” “consensus
|
||||
attack,” “reentrancy attack,” “griefing attack”, “economic
|
||||
incentive attack” or theft, conversion or
|
||||
misappropriation of tokens or other similar action;
|
||||
|
||||
i. any “front-running,” “wash trading,” “pump and dump trading,”
|
||||
“ramping,” “cornering” or other illegal, fraudulent, deceptive
|
||||
or manipulative trading activities ;
|
||||
|
||||
j. any device, plan, scheme or artifice to unfairly or deceptively
|
||||
influence the market price of any token; or
|
||||
|
||||
k. modifying or making derivative works based on the Web Application.
|
||||
|
||||
4. REPRESENTATIONS OF LICENSEES.
|
||||
Each person making use of or relying on any license granted under Section 2
|
||||
(each, a “Licensee”) hereby represents and warrants to Licensor that the
|
||||
following statements and information are accurate and complete at all times
|
||||
that such person makes use of or relies on the license.
|
||||
|
||||
a. Status. If Licensee is an individual, Licensee is of legal age in
|
||||
the jurisdiction in which Licensee resides (and in any event is
|
||||
older than thirteen years of age) and is of sound mind. If
|
||||
Licensee is a business entity, Licensee is duly organized, validly
|
||||
existing and in good standing under the laws of the jurisdiction
|
||||
in which it is organized and has all requisite power and authority
|
||||
for a business entity of its type to carry on its business as now
|
||||
conducted.
|
||||
|
||||
b. Power and Authority. Licensee has all requisite capacity, power and
|
||||
authority to accept this Agreement and to carry out and perform its
|
||||
obligations under this Agreement. This Agreement constitutes a
|
||||
legal, valid and binding obligation of Licensee, enforceable
|
||||
against Licensee.
|
||||
|
||||
c. No Conflict; Compliance with Law. Licensee agreeing to this
|
||||
Agreement and using the Web Application does not constitute, and
|
||||
would not reasonably be expected to result in (with or without
|
||||
notice, lapse of time, or both), a breach, default, contravention
|
||||
or violation of any law applicable to Licensee, or contract or
|
||||
agreement to which Licensee is a party or by which Licensee is
|
||||
bound.
|
||||
|
||||
d. Absence of Sanctions. Licensee is not, (and, if Licensee is an
|
||||
entity, Licensee is not owned or controlled by any other person
|
||||
who is), and is not acting on behalf of any other person who is,
|
||||
identified on any list of prohibited parties under any law or by
|
||||
any nation or government, state or other political subdivision
|
||||
thereof, any entity exercising legislative, judicial or
|
||||
administrative functions of or pertaining to government such as
|
||||
the lists maintained by the United Nations Security Council, the
|
||||
United Kingdom, the British Virgin Islands, the United States
|
||||
(including the U.S. Treasury Department’s Specially Designated
|
||||
Nationals list and Foreign Sanctions Evaders list), the European
|
||||
Union (EU) or its member states, and the government of a Licensee
|
||||
home country. Licensee is not, (and, if Licensee is an entity,
|
||||
Licensee is not owned or controlled by any other person who is),
|
||||
and is not acting on behalf of any other person who is, located,
|
||||
ordinarily resident, organized, established, or domiciled in Cuba,
|
||||
Iran, North Korea, Sudan, Syria, the Crimea region (including
|
||||
Sevastopol) or any other country or jurisdiction against which the
|
||||
United Nations, the United Kingdom, the British Virgin Islands or
|
||||
the United States maintains economic sanctions or an arms embargo.
|
||||
The tokens or other funds a Licensee use to participate in the Web
|
||||
Application are not derived from, and do not otherwise represent
|
||||
the proceeds of, any activities done in violation or contravention
|
||||
of any law.
|
||||
|
||||
e. No Claim, Loan, Ownership Interest or Investment Purpose. Licensee
|
||||
understands and agrees that the Licensee’s use of the Web
|
||||
Application does not: (i) represent or constitute a loan or a
|
||||
contribution of capital to, or other investment in Licensor or any
|
||||
business or venture; (ii) provide Licensee with any ownership
|
||||
interest, equity, security, or right to or interest in the assets,
|
||||
rights, properties, revenues or profits of, or voting rights
|
||||
whatsoever in, Licensor or any other business or venture; or (iii)
|
||||
create or imply or entitle Licensee to the benefits of any
|
||||
fiduciary or other agency relationship between Licensor or any of
|
||||
its directors, officers, employees, agents or affiliates, on the
|
||||
on hand, and Licensee, on the other hand. Licensee is not entering
|
||||
into this Agreement or using the Web Application for the purpose
|
||||
of making an investment with respect to Licensor or its securities,
|
||||
but solely wishes to use the Web Application for their
|
||||
intended purposes. Licensee understands and agrees that Licensor
|
||||
will not accept or take custody over any tokens or money or other
|
||||
assets of Licensee and has no responsibility or control over the
|
||||
foregoing.
|
||||
|
||||
f. Non-Reliance. Licensee is knowledgeable, experienced and
|
||||
sophisticated in using and evaluating blockchain and related
|
||||
technologies and assets, including all technologies referenced
|
||||
herein. Licensee has conducted its own thorough independent
|
||||
investigation and analysis of the Web Application and the other
|
||||
matters contemplated by this Agreement, and has not relied upon
|
||||
any information, statement, omission, representation or warranty,
|
||||
express or implied, written or oral, made by or on behalf of
|
||||
Licensor in connection therewith.
|
||||
|
||||
5. RISKS, DISCLAIMERS AND LIMITATIONS OF LIABILITY.
|
||||
THE WEB APPLICATION IS PROVIDED "AS IS" AND “AS-AVAILABLE,” AND ANY
|
||||
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE
|
||||
HEREBY DISCLAIMED. IN NO EVENT SHALL LICENSOR OR ANY OTHER CONTRIBUTOR TO
|
||||
THE WEB APPLICATION BE LIABLE FOR ANY DAMAGES, INCLUDING ANY DIRECT,
|
||||
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ARISING
|
||||
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE OR INTELLECTUAL PROPERTY
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION),
|
||||
HOWEVER CAUSED OR CLAIMED (WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE)), EVEN IF SUCH DAMAGES WERE REASONABLY
|
||||
FORESEEABLE OR THE COPYRIGHT HOLDERS AND CONTRIBUTORS WERE ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
6. GENERAL PROVISIONS
|
||||
|
||||
a. Governing Law. This Agreement shall be governed by and construed
|
||||
under the internal laws of the British Virgin Islands, regardless
|
||||
of the laws that might otherwise govern under applicable
|
||||
principles of conflicts of laws.
|
||||
|
||||
b. Dispute Resolution. Licensee (i) hereby irrevocably and
|
||||
unconditionally submits to the jurisdiction of the relevant courts
|
||||
of the British Virgin Islands for the purpose of any dispute,
|
||||
suit, action or other proceeding arising out of or based upon this
|
||||
Agreement or the matters contemplated by this Agreement
|
||||
(“Disputes”), (ii) agrees not to commence any suit, action or
|
||||
other proceeding arising in connection with or based upon this
|
||||
Agreement or the matters contemplated by this Agreement except
|
||||
before the relevant courts of the British Virgin Islands, and
|
||||
(iii) hereby waives, and agrees not to assert, by way of motion,
|
||||
as a defense, or otherwise, in any such suit, action or
|
||||
proceeding, any claim that it is not subject personally to the
|
||||
jurisdiction of the above-named courts, that its property is
|
||||
exempt or immune from attachment or execution, that the suit,
|
||||
action or proceeding is brought in an inconvenient forum, that the
|
||||
venue of the suit, action or proceeding is improper or that this
|
||||
Agreement or the subject matter hereof or thereof may not be
|
||||
enforced in or by such court.
|
||||
|
||||
Each party will bear its own costs in respect of any Disputes.
|
||||
|
||||
Notwithstanding the foregoing, at the Licensor’s sole option and
|
||||
commencing within a reasonable period from the date of
|
||||
notification to the other party of such Dispute, any Dispute may
|
||||
be resolved by confidential, binding arbitration to be seated in
|
||||
the British Virgin Islands and conducted in the English language
|
||||
by a single arbitrator pursuant to the rules of the International
|
||||
Chamber of Commerce (the “Rules”). The arbitrator shall be
|
||||
appointed in accordance with the procedures set out in the Rules.
|
||||
The award or decision of the arbitrator shall be final and binding
|
||||
upon the parties and the parties expressly waive any right under
|
||||
the laws of any jurisdiction to appeal or otherwise challenge the
|
||||
award, ruling or decision of the arbitrator. The judgment of any
|
||||
award or decision may be entered in any court having competent
|
||||
jurisdiction to the extent necessary. If the Licensor elects to
|
||||
have a Dispute resolved by arbitration pursuant to this provision,
|
||||
no party hereto shall (or shall permit its representatives to)
|
||||
commence, continue or pursue any Dispute in any court.
|
||||
|
||||
Notwithstanding anything to the contrary set forth in this
|
||||
Agreement, the Licensor shall at all times be entitled to obtain
|
||||
an injunction or injunctions to prevent breaches of this Agreement
|
||||
and to enforce specifically the terms and provisions thereof, this
|
||||
being in addition to any other remedy to which the Licensor is
|
||||
entitled at law or in equity, and the parties hereto hereby waive
|
||||
the requirement of any undertaking in damages or posting of a bond
|
||||
in connection with such injunctive relief or specific performance.
|
||||
|
||||
EACH PARTY HEREBY WAIVES ITS RIGHTS TO A JURY TRIAL OF ANY CLAIM
|
||||
OR CAUSE OF ACTION BASED UPON OR ARISING OUT OF THIS AGREEMENT OR
|
||||
THE SUBJECT MATTER HEREOF. THE SCOPE OF THIS WAIVER IS INTENDED TO
|
||||
BE ALL-ENCOMPASSING OF ANY AND ALL DISPUTES THAT MAY BE FILED IN
|
||||
ANY COURT AND THAT RELATE TO THE SUBJECT MATTER OF ANY OF THE
|
||||
TRANSACTIONS CONTEMPLATED BY THIS AGREEMENT, INCLUDING, WITHOUT
|
||||
LIMITATION, CONTRACT CLAIMS, TORT CLAIMS (INCLUDING NEGLIGENCE),
|
||||
BREACH OF DUTY CLAIMS, AND ALL OTHER COMMON LAW AND STATUTORY
|
||||
CLAIMS. THIS SECTION HAS BEEN FULLY DISCUSSED BY EACH OF THE
|
||||
PARTIES HERETO AND THESE PROVISIONS WILL NOT BE SUBJECT TO ANY
|
||||
EXCEPTIONS. EACH PARTY HERETO HEREBY FURTHER WARRANTS AND
|
||||
REPRESENTS THAT SUCH PARTY HAS REVIEWED THIS WAIVER WITH ITS LEGAL
|
||||
COUNSEL, AND THAT SUCH PARTY KNOWINGLY AND VOLUNTARILY WAIVES ITS
|
||||
JURY TRIAL RIGHTS FOLLOWING CONSULTATION WITH LEGAL COUNSEL.
|
||||
|
||||
c. Class Action Waiver. No Class Actions Permitted. All Licensees
|
||||
hereby agree that any arbitration or other permitted action with
|
||||
respect to any Dispute shall be conducted in their individual
|
||||
capacities only and not as a class action or other representative
|
||||
action, and the Licensees expressly waive their right to file a
|
||||
class action or seek relief on a class basis. LICENSEES SHALL
|
||||
BRING CLAIMS AGAINST LICENSOR ONLY IN THEIR INDIVIDUAL CAPACITY,
|
||||
AND NOT AS A PLAINTIFF OR CLASS MEMBER IN ANY PURPORTED CLASS OR
|
||||
REPRESENTATIVE PROCEEDING.
|
||||
|
||||
Agreements if Class Action Waiver Unenforceable. If any court or
|
||||
arbitrator makes a final, binding and non-appealable determination
|
||||
that the class action waiver set forth herein is void or
|
||||
unenforceable for any reason or that a Dispute can proceed on a
|
||||
class basis, then the arbitration provision set forth above shall
|
||||
be deemed null and void with respect to any Dispute that would
|
||||
thus be required to be resolved by arbitration on a class basis,
|
||||
and the parties shall be deemed to have not agreed to arbitrate
|
||||
such Dispute. In the event that, as a result of the application of
|
||||
the immediately preceding sentence or otherwise, any Dispute is
|
||||
not subject to arbitration, the parties hereby agree to submit to
|
||||
the personal and exclusive jurisdiction of and venue in the
|
||||
federal and state courts located in the British Virgin Islands and
|
||||
to accept service of process by mail with respect to such Dispute,
|
||||
and hereby waive any and all jurisdictional and venue defenses
|
||||
otherwise available with respect to such Dispute.
|
||||
|
||||
d. Amendment; Waiver. This Agreement may be amended and provisions
|
||||
may be waived (either generally or in a particular instance and
|
||||
either retroactively or prospectively), only with the written
|
||||
consent of the Licensor.
|
||||
|
||||
e. Severability. Any term or provision of this Agreement that is
|
||||
found invalid or unenforceable in any situation in any
|
||||
jurisdiction shall not affect the validity or enforceability of
|
||||
the remaining terms and provisions hereof or the validity or
|
||||
enforceability of the offending term or provision in any other
|
||||
situation or in any other jurisdiction. If a final judgment of a
|
||||
court of competent jurisdiction declares that any term or
|
||||
provision hereof is invalid or unenforceable, the parties hereto
|
||||
agree that the court making such determination shall have the
|
||||
power to limit such term or provision, to delete specific words
|
||||
or phrases, or to replace any invalid or unenforceable term or
|
||||
provision with a term or provision that is valid and enforceable
|
||||
and that comes closest to expressing the intention of the invalid
|
||||
or unenforceable term or provision, and this Agreement shall be
|
||||
enforceable as so modified. In the event such court does not
|
||||
exercise the power granted to it in the prior sentence, the
|
||||
parties hereto agree to replace such invalid or unenforceable term
|
||||
or provision with a valid and enforceable term or provision that
|
||||
will achieve, to the extent possible, the economic, business and
|
||||
other purposes of such invalid or unenforceable term or provision.
|
||||
|
||||
f. Entire Agreement. This Agreement constitutes the entire agreement
|
||||
and understanding of the parties with respect to the subject matter
|
||||
hereof and supersede any and all prior negotiations,
|
||||
correspondence, warrants, agreements, understandings duties or
|
||||
obligations between or involving the parties with respect to the
|
||||
subject matter hereof.
|
||||
|
||||
g. Delays or Omissions. No delay or omission to exercise any right,
|
||||
power, or remedy accruing to any party under this Agreement, upon
|
||||
any breach or default of any other party under this Agreement,
|
||||
shall impair any such right, power, or remedy of such
|
||||
non-breaching or non-defaulting party, nor shall it be construed
|
||||
to be a waiver of or acquiescence to any such breach or default,
|
||||
or to any similar breach or default thereafter occurring, nor
|
||||
shall any waiver of any single breach or default be deemed a
|
||||
waiver of any other breach or default theretofore or thereafter
|
||||
occurring. Any waiver, permit, consent or approval of any kind or
|
||||
character on the part of any party of any breach or default under
|
||||
this Agreement, or any waiver on the part of any party of any
|
||||
provisions or conditions of this Agreement, must be in writing and
|
||||
shall be effective only to the extent specifically set forth in
|
||||
such writing. All remedies, whether under this Agreement or by law
|
||||
or otherwise afforded to any party, shall be cumulative and not
|
||||
alternative.
|
||||
|
||||
h. Successors and Assigns. The terms and conditions of this Agreement
|
||||
shall inure to the benefit of and be binding upon the respective
|
||||
successors and assigns of the parties hereto. This Agreement shall
|
||||
not have third-party beneficiaries, other than the Martian Council.
|
||||
|
||||
i. Rules of Construction. Gender; Etc. For purposes of this
|
||||
Agreement, whenever the context requires: the singular number
|
||||
shall include the plural, and vice versa; the masculine gender
|
||||
shall include the feminine and neuter genders; the feminine gender
|
||||
shall include the masculine and neuter genders; and the neuter
|
||||
gender shall include the masculine and feminine genders.
|
||||
|
||||
Ambiguities. The Parties hereto agree that any rule of
|
||||
construction to the effect that ambiguities are to be resolved
|
||||
against the drafting Party shall not be applied in the
|
||||
construction or interpretation of this Agreement.
|
||||
|
||||
No Limitation. As used in this Agreement, the words “include,”
|
||||
“including,” “such as” and variations thereof, shall not be deemed
|
||||
to be terms of limitation, but rather shall be deemed to be
|
||||
followed by the words “without limitation.” The word “or” shall
|
||||
mean the non-exclusive “or”. References. Except as otherwise
|
||||
indicated, all references in this Agreement to “Sections,”
|
||||
“Schedules” and “Exhibits” are intended to refer to Sections of
|
||||
this Agreement and Schedules and Exhibits to this Agreement.
|
||||
|
||||
Hereof. The terms “hereof,” “herein,” “hereunder,” “hereby” and
|
||||
“herewith” and words of similar import will, unless otherwise
|
||||
stated, be construed to refer to this Agreement as a whole and not
|
||||
to any particular provision of this Agreement.
|
||||
|
||||
Captions/Headings. The captions, headings and similar labels
|
||||
contained in this Agreement are for convenience of reference only,
|
||||
shall not be deemed to be a part of this Agreement and shall not
|
||||
be referred to in connection with the construction or
|
||||
interpretation of this Agreement.
|
||||
|
||||
Person. The term “person” refers to any natural born or legal
|
||||
person, entity, governmental body or incorporated or
|
||||
unincorporated association, partnership or joint venture.
|
Binary file not shown.
51
README.md
51
README.md
@ -1,28 +1,47 @@
|
||||
# Mars Protocol Interface
|
||||
# Mars Protocol Osmosis Outpost Frontend
|
||||
|
||||
## Terms of Use
|
||||
![mars-banner-1200w](https://marsprotocol.io/banner.png)
|
||||
|
||||
To use this repository you have to agree to the terms of the [Mars Web App License](https://github.com/mars-protocol/interface/blob/main/Mars%20Web%20App%20License.pdf)
|
||||
## Web App
|
||||
|
||||
## Available Scripts
|
||||
This project is a [NextJS](https://nextjs.org/). React application.
|
||||
|
||||
In the project directory, you can run:
|
||||
The project utilises [React hooks](https://reactjs.org/docs/hooks-intro.html), functional components, Zustand for state management, and useQuery for general data fetching and management.
|
||||
|
||||
### `npm run install`
|
||||
Typescript is added and utilised (but optional if you want to create .jsx or .tsx files).
|
||||
|
||||
Installs all packages listed inside the [package.json](https://github.com/mars-protocol/interface/blob/main/package.json)
|
||||
SCSS with [CSS modules](https://create-react-app.dev/docs/adding-a-css-modules-stylesheet) (webpack allows importing css files into javascript, use the CSS module technique to avoid className clashes).
|
||||
|
||||
### `npm run start`
|
||||
Sentry is used for front end error logging/exception & bug reporting.
|
||||
|
||||
Runs the app in the development mode.
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
## Deployment
|
||||
|
||||
The page will reload if you make edits.
|
||||
Start web server
|
||||
|
||||
### `npm run build`
|
||||
```bash
|
||||
yarn && yarn dev
|
||||
```
|
||||
|
||||
Builds the app for production to the `build` folder.
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
### Contributing
|
||||
|
||||
The build is minified and the filenames include the hashes.
|
||||
Your app is ready to be deployed!
|
||||
We welcome and encourage contributions! Please create a pull request with as much information about the work you did and what your motivation/intention was.
|
||||
|
||||
## Imports
|
||||
|
||||
Local components are imported via index files, which can be automatically generated with `yarn index`. This command targets index.ts files with a specific pattern in order to automate component exports. This results in clean imports throughout the pages:
|
||||
|
||||
```
|
||||
import { Button, Card, Titlte } from 'components/common'
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
import { Breakdown, RepayInput } from 'components/fields'
|
||||
```
|
||||
|
||||
In order for this to work, components are place in a folder with UpperCamelCase with the respective Component.tsx file. The component cannot be exported at default, so rather export the `const` instead.
|
||||
|
||||
## License
|
||||
|
||||
Contents of this repository are open source under the [Mars Protocol Web Application License Agreement](./LICENSE).
|
||||
|
21
jest.config.js
Normal file
21
jest.config.js
Normal file
@ -0,0 +1,21 @@
|
||||
// jest.config.js
|
||||
const nextJest = require('next/jest')
|
||||
|
||||
const createJestConfig = nextJest({
|
||||
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
|
||||
dir: './',
|
||||
})
|
||||
|
||||
// Add any custom config to be passed to Jest
|
||||
/** @type {import('jest').Config} */
|
||||
const customJestConfig = {
|
||||
// Add more setup options before each test is run
|
||||
// setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||
// if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
|
||||
moduleDirectories: ['node_modules', '<rootDir>/src'],
|
||||
testEnvironment: 'jest-environment-jsdom',
|
||||
collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'],
|
||||
}
|
||||
|
||||
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
||||
module.exports = createJestConfig(customJestConfig)
|
5
next-env.d.ts
vendored
Normal file
5
next-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
67
next.config.js
Normal file
67
next.config.js
Normal file
@ -0,0 +1,67 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
|
||||
const { withSentryConfig } = require('@sentry/nextjs')
|
||||
const path = require('path')
|
||||
|
||||
const moduleExports = {
|
||||
reactStrictMode: true,
|
||||
experimental: { images: { unoptimized: true } },
|
||||
sassOptions: {
|
||||
includePaths: [path.join(__dirname, 'src/styles')],
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/',
|
||||
destination: '/redbank',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/farm/vault/:address/create',
|
||||
destination: '/farm/',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/farm/vault/:address/create/setup',
|
||||
destination: '/farm/',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/farm/vault/:address/edit',
|
||||
destination: '/farm/',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/farm/vault/:address/unlock',
|
||||
destination: '/farm',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/farm/vault/:address/close',
|
||||
destination: '/farm',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/farm/vault/:address/repay',
|
||||
destination: '/farm',
|
||||
permanent: true,
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
const sentryWebpackPluginOptions = {
|
||||
// Additional config options for the Sentry Webpack plugin. Keep in mind that
|
||||
// the following options are set automatically, and overriding them is not
|
||||
// recommended:
|
||||
// release, url, org, project, authToken, configFile, stripPrefix,
|
||||
// urlPrefix, include, ignore
|
||||
|
||||
silent: true, // Suppresses all logs
|
||||
// For all available options, see:
|
||||
// https://github.com/getsentry/sentry-webpack-plugin#options.
|
||||
}
|
||||
|
||||
// Make sure adding Sentry options is the last code to run before exporting, to
|
||||
// ensure that your source maps include changes from all other Webpack plugins
|
||||
module.exports = withSentryConfig(moduleExports, sentryWebpackPluginOptions)
|
42050
package-lock.json
generated
42050
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
198
package.json
198
package.json
@ -1,94 +1,108 @@
|
||||
{
|
||||
"name": "mars",
|
||||
"homepage": "./",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.4.15",
|
||||
"@material-ui/core": "^4.11.3",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@ramonak/react-progress-bar": "^4.2.0",
|
||||
"@sentry/react": "^6.17.9",
|
||||
"@sentry/tracing": "^6.17.9",
|
||||
"@terra-dev/wallet-types": "^3.2.0",
|
||||
"@terra-money/terra.js": "^3.0.1",
|
||||
"@terra-money/wallet-provider": "^3.8.0",
|
||||
"@testing-library/dom": "^8.11.1",
|
||||
"@testing-library/jest-dom": "^5.11.4",
|
||||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"@tippyjs/react": "^4.2.3",
|
||||
"@tns-money/tns.js": "^1.2.0",
|
||||
"@types/chart.js": "^2.9.31",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/lodash.throttle": "^4.1.6",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/numeral": "^2.0.1",
|
||||
"@types/ramda": "^0.27.38",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/react-modal": "^3.12.0",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"@types/react-table": "^7.0.28",
|
||||
"bignumber.js": "^9.0.1",
|
||||
"chart.js": "^2.9.4",
|
||||
"create-conical-gradient": "^1.1.0",
|
||||
"graphql": "^15.6.0",
|
||||
"graphql-request": "^4.2.0",
|
||||
"i18next": "^21.0.2",
|
||||
"i18next-browser-languagedetector": "^6.1.2",
|
||||
"i18next-xhr-backend": "^3.2.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"moment": "^2.29.1",
|
||||
"moment-duration-format": "^2.3.2",
|
||||
"numeral": "^2.0.6",
|
||||
"ramda": "^0.27.1",
|
||||
"react": "^17.0.1",
|
||||
"react-chartjs-2": "^2.11.1",
|
||||
"react-currency-input-field": "^3.6.4",
|
||||
"react-device-detect": "^1.17.0",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-i18next": "^11.11.4",
|
||||
"react-modal": "^3.12.1",
|
||||
"react-query": "^3.34.19",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "^4.0.3",
|
||||
"react-table": "^7.6.3",
|
||||
"react-use-clipboard": "^1.0.7",
|
||||
"sass": "^1.35.2",
|
||||
"styled-components": "^5.3.3",
|
||||
"typescript": "^4.1.2",
|
||||
"web-vitals": "^1.0.1",
|
||||
"zustand": "^3.7.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "REACT_APP_STAGE=localhost react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash.isequal": "^4.5.5",
|
||||
"prettier": "^2.5.1",
|
||||
"pretty-quick": "^3.1.3"
|
||||
}
|
||||
"name": "mars",
|
||||
"homepage": "./",
|
||||
"version": "1.0.0",
|
||||
"private": false,
|
||||
"license": "SEE LICENSE IN LICENSE FILE",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "yarn test && next build",
|
||||
"export": "next export",
|
||||
"start": "next start",
|
||||
"lint": "eslint ./src/ && yarn prettier-check",
|
||||
"format": "yarn index && eslint ./src/ --fix && prettier --write ./src/",
|
||||
"prettier-check": "prettier --ignore-path .gitignore --check ./src/",
|
||||
"index": "vscode-generate-index-standalone src/",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cosmjs/cosmwasm-stargate": "^0.29.5",
|
||||
"@cosmjs/launchpad": "^0.27.1",
|
||||
"@cosmjs/proto-signing": "^0.29.5",
|
||||
"@cosmjs/stargate": "^0.29.5",
|
||||
"@marsprotocol/wallet-connector": "^0.9.12",
|
||||
"@material-ui/core": "^4.12.4",
|
||||
"@material-ui/icons": "^4.11.3",
|
||||
"@ramonak/react-progress-bar": "^5.0.2",
|
||||
"@sentry/nextjs": "^7.12.1",
|
||||
"@tanstack/react-query": "^4.3.4",
|
||||
"@tanstack/react-table": "^8.5.13",
|
||||
"@testing-library/dom": "^8.17.1",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"bignumber.js": "^9.1.0",
|
||||
"chart.js": "^3.9.1",
|
||||
"classnames": "^2.3.1",
|
||||
"create-conical-gradient": "^1.1.0",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^5.0.0",
|
||||
"i18next": "^21.9.1",
|
||||
"i18next-browser-languagedetector": "^6.1.5",
|
||||
"i18next-http-backend": "^1.4.1",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"moment": "^2.29.4",
|
||||
"moment-duration-format": "^2.3.2",
|
||||
"next": "^12.2.5",
|
||||
"numeral": "^2.0.6",
|
||||
"ramda": "^0.28.0",
|
||||
"react": "^18.2.0",
|
||||
"react-chartjs-2": "^4.3.1",
|
||||
"react-currency-input-field": "^3.6.4",
|
||||
"react-device-detect": "^2.2.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^11.18.5",
|
||||
"react-spring": "^9.5.5",
|
||||
"react-table": "^7.8.0",
|
||||
"react-use-clipboard": "^1.0.8",
|
||||
"sass": "^1.56.1",
|
||||
"typescript": "^4.8.2",
|
||||
"web-vitals": "^3.0.1",
|
||||
"zustand": "^4.1.1"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chart.js": "^2.9.37",
|
||||
"@types/classnames": "^2.3.1",
|
||||
"@types/jest": "^29.2.3",
|
||||
"@types/lodash.clonedeep": "^4.5.7",
|
||||
"@types/lodash.isequal": "^4.5.6",
|
||||
"@types/lodash.throttle": "^4.1.7",
|
||||
"@types/node": "^18.7.15",
|
||||
"@types/numeral": "^2.0.2",
|
||||
"@types/prettier": "^2.7.0",
|
||||
"@types/ramda": "^0.28.15",
|
||||
"@types/react": "^18.0.18",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-table": "^7.7.12",
|
||||
"eslint": "^8.23.0",
|
||||
"eslint-config-next": "^12.2.5",
|
||||
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"jest": "^29.3.1",
|
||||
"jest-environment-jsdom": "^29.3.1",
|
||||
"prettier": "^2.7.1",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"vscode-generate-index-standalone": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"npm": "please-use-yarn",
|
||||
"yarn": ">= 1.19.1"
|
||||
}
|
||||
}
|
||||
|
@ -1,35 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="https://app.marsprotocol.io/favicon.svg" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#dd5b65" />
|
||||
<meta name="robots" content="index,follow" />
|
||||
<meta name="description"
|
||||
content="Lend, borrow and earn on the galaxy's most powerful credit protocol or enter the Fields of Mars for advanced DeFi strategies." />
|
||||
<meta
|
||||
name="description"
|
||||
content="Lend, borrow and earn on the galaxy's most powerful credit protocol or enter the Fields of Mars for advanced DeFi strategies."
|
||||
/>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@mars_protocol" />
|
||||
<meta name="twitter:creator" content="@mars_protocol" />
|
||||
<meta property="og:url" content="https://app.marsprotocol.io" />
|
||||
<meta property="og:title" content="Mars Protocol Application - Powered by Terra" />
|
||||
<meta property="og:description"
|
||||
content="Lend, borrow and earn on the galaxy's most powerful credit protocol or enter the Fields of Mars for advanced DeFi strategies." />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Lend, borrow and earn on the galaxy's most powerful credit protocol or enter the Fields of Mars for advanced DeFi strategies."
|
||||
/>
|
||||
<meta property="og:image" content="https://app.marsprotocol.io/banner.png" />
|
||||
<meta property="og:site_name" content="Mars Protocol" />
|
||||
<meta name="msapplication-TileColor" content="#ffffff" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<meta name="terra-webextension" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no" />
|
||||
<title>Mars Protocol</title>
|
||||
</head>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root">
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,19 +1,19 @@
|
||||
{
|
||||
"name": "Mars",
|
||||
"short_name": "Mars",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
"name": "Mars",
|
||||
"short_name": "Mars",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
|
19
sentry.client.config.js
Normal file
19
sentry.client.config.js
Normal file
@ -0,0 +1,19 @@
|
||||
// This file configures the initialization of Sentry on the browser.
|
||||
// The config you add here will be used whenever a page is visited.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
|
||||
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN
|
||||
|
||||
Sentry.init({
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENV,
|
||||
dsn: SENTRY_DSN,
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 0.5,
|
||||
// ...
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
enabled: process.env.NODE_ENV !== 'development',
|
||||
})
|
4
sentry.properties
Normal file
4
sentry.properties
Normal file
@ -0,0 +1,4 @@
|
||||
defaults.url=https://sentry.io/
|
||||
defaults.org=delphi-mars
|
||||
defaults.project=mars-dapp
|
||||
cli.executable=../../../.npm/_npx/a8388072043b4cbc/node_modules/@sentry/cli/bin/sentry-cli
|
18
sentry.server.config.js
Normal file
18
sentry.server.config.js
Normal file
@ -0,0 +1,18 @@
|
||||
// This file configures the initialization of Sentry on the server.
|
||||
// The config you add here will be used whenever the server handles a request.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from '@sentry/nextjs'
|
||||
|
||||
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 0.5,
|
||||
// ...
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
enabled: process.env.NODE_ENV !== 'development',
|
||||
})
|
@ -1,191 +0,0 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.button {
|
||||
transition: background-color 0.2s, border 0.2s;
|
||||
color: $fontColorLightPrimary;
|
||||
appearance: none;
|
||||
border: none;
|
||||
border-radius: $borderRadiusXXXL;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
word-wrap: normal;
|
||||
word-break: normal;
|
||||
|
||||
.prefix,
|
||||
.suffix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
// Sizes
|
||||
&.small {
|
||||
@include buttonS;
|
||||
|
||||
svg,
|
||||
.progressIndicator {
|
||||
width: rem-calc(10);
|
||||
height: rem-calc(10);
|
||||
}
|
||||
|
||||
.prefix {
|
||||
margin-inline-end: space(2);
|
||||
}
|
||||
|
||||
.suffix {
|
||||
margin-inline-start: space(2);
|
||||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
@include buttonM;
|
||||
|
||||
svg {
|
||||
width: rem-calc(12);
|
||||
height: rem-calc(12);
|
||||
}
|
||||
|
||||
.prefix {
|
||||
margin-inline-end: space(3);
|
||||
}
|
||||
|
||||
.suffix {
|
||||
width: rem-calc(12);
|
||||
margin-inline-start: space(3);
|
||||
}
|
||||
}
|
||||
|
||||
&.large {
|
||||
@include buttonL;
|
||||
svg {
|
||||
width: rem-calc(18);
|
||||
height: rem-calc(18);
|
||||
}
|
||||
|
||||
.prefix {
|
||||
margin-inline-end: space(4.5);
|
||||
}
|
||||
|
||||
.suffix {
|
||||
margin-inline-start: space(4.5);
|
||||
}
|
||||
}
|
||||
|
||||
// Variants
|
||||
&.solid,
|
||||
&.round {
|
||||
@include buttonSolidPrimary;
|
||||
@include buttonSolidSecondary;
|
||||
@include buttonSolidTertiary;
|
||||
}
|
||||
|
||||
&.round {
|
||||
border-radius: $borderRadiusRound;
|
||||
@include padding(0);
|
||||
|
||||
.prefix,
|
||||
.suffix {
|
||||
@include margin(0);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&.small {
|
||||
height: rem-calc(32);
|
||||
width: rem-calc(32);
|
||||
|
||||
svg {
|
||||
width: rem-calc(12);
|
||||
height: rem-calc(12);
|
||||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
height: rem-calc(40);
|
||||
width: rem-calc(40);
|
||||
|
||||
svg {
|
||||
width: rem-calc(14);
|
||||
height: rem-calc(14);
|
||||
}
|
||||
}
|
||||
|
||||
&.large {
|
||||
height: rem-calc(56);
|
||||
width: rem-calc(56);
|
||||
|
||||
svg {
|
||||
width: rem-calc(20);
|
||||
height: rem-calc(20);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.transparent {
|
||||
background: none;
|
||||
@include padding(0);
|
||||
height: unset;
|
||||
|
||||
&.primary {
|
||||
color: $colorPrimary;
|
||||
|
||||
* {
|
||||
color: $colorPrimary;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $colorPrimaryHighlight;
|
||||
|
||||
* {
|
||||
color: $colorPrimaryHighlight;
|
||||
}
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
color: $colorPrimaryHighlight;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
color: $colorSecondary;
|
||||
|
||||
* {
|
||||
color: $colorSecondary;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
color: $colorSecondaryHighlight;
|
||||
}
|
||||
}
|
||||
|
||||
&.tertiary {
|
||||
color: $colorSecondaryDark;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
color: lighten($colorSecondaryDark, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5 !important;
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
import { CircularProgress } from '@material-ui/core'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
import { ButtonStyleOverride } from '../types/components'
|
||||
import styles from './Button.module.scss'
|
||||
|
||||
interface Props {
|
||||
className?: any
|
||||
color?: 'primary' | 'secondary' | 'tertiary'
|
||||
disabled?: boolean
|
||||
externalLink?: string
|
||||
id?: string
|
||||
suffix?: ReactNode
|
||||
prefix?: ReactNode
|
||||
showProgressIndicator?: boolean
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
styleOverride?: ButtonStyleOverride
|
||||
text?: string | ReactNode
|
||||
variant?: 'solid' | 'transparent' | 'round'
|
||||
onClick?: (e: any) => void
|
||||
}
|
||||
|
||||
const Button = ({
|
||||
className = '',
|
||||
color = 'primary',
|
||||
disabled,
|
||||
externalLink,
|
||||
id = '',
|
||||
suffix,
|
||||
prefix,
|
||||
showProgressIndicator,
|
||||
size = 'small',
|
||||
styleOverride,
|
||||
text,
|
||||
variant = 'solid',
|
||||
onClick,
|
||||
}: Props) => {
|
||||
const Button = () => (
|
||||
<button
|
||||
id={id}
|
||||
onClick={disabled ? () => {} : onClick}
|
||||
style={styleOverride}
|
||||
className={`${styles.button} ${styles[size]} ${styles[color]} ${
|
||||
styles[variant]
|
||||
} ${className} ${disabled ? `${styles.disabled}` : ''}`}
|
||||
>
|
||||
{prefix && !showProgressIndicator && (
|
||||
<div className={styles.prefix}>{prefix}</div>
|
||||
)}
|
||||
{text && (
|
||||
<div className={styles.text}>
|
||||
{showProgressIndicator ? (
|
||||
<CircularProgress
|
||||
color='inherit'
|
||||
size={
|
||||
size === 'small'
|
||||
? '10px'
|
||||
: size === 'medium'
|
||||
? '12px'
|
||||
: '18px'
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
text
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{suffix && !showProgressIndicator && (
|
||||
<div className={styles.suffix}>{suffix}</div>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
|
||||
return externalLink ? (
|
||||
<a
|
||||
href={externalLink}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className={styles.link}
|
||||
>
|
||||
{Button()}
|
||||
</a>
|
||||
) : (
|
||||
Button()
|
||||
)
|
||||
}
|
||||
|
||||
export default Button
|
@ -1,52 +0,0 @@
|
||||
import { createStyles, Switch, withStyles } from '@material-ui/core'
|
||||
import colors from '../styles/_assets.module.scss'
|
||||
|
||||
interface Props {
|
||||
switchCallback: (
|
||||
event: React.ChangeEvent<HTMLInputElement>,
|
||||
enabled: boolean
|
||||
) => void
|
||||
checked: boolean
|
||||
}
|
||||
|
||||
const CollateralSwitch = ({ switchCallback, checked }: Props) => {
|
||||
const CustomSwitch = withStyles(() =>
|
||||
createStyles({
|
||||
root: {
|
||||
width: 28,
|
||||
height: 16,
|
||||
padding: 0,
|
||||
display: 'flex',
|
||||
},
|
||||
switchBase: {
|
||||
padding: 2,
|
||||
color: colors.grey,
|
||||
'&$checked': {
|
||||
transform: 'translateX(12px)',
|
||||
color: colors.white,
|
||||
'& + $track': {
|
||||
opacity: 1,
|
||||
backgroundColor: colors.primary,
|
||||
borderColor: colors.primary,
|
||||
},
|
||||
},
|
||||
},
|
||||
thumb: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
boxShadow: 'none',
|
||||
},
|
||||
track: {
|
||||
border: `1px solid ${colors.grey}`,
|
||||
borderRadius: 16 / 2,
|
||||
opacity: 1,
|
||||
backgroundColor: colors.white,
|
||||
},
|
||||
checked: {},
|
||||
})
|
||||
)(Switch)
|
||||
|
||||
return <CustomSwitch checked={checked} onChange={switchCallback} />
|
||||
}
|
||||
|
||||
export default CollateralSwitch
|
@ -1,70 +0,0 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.container {
|
||||
@include layoutTooltip;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: rem-calc(184);
|
||||
box-sizing: unset;
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
|
||||
.valueItem {
|
||||
margin-top: space(1);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.dot {
|
||||
border: 1px solid $colorWhite;
|
||||
height: rem-calc(8);
|
||||
border-radius: $borderRadiusRound;
|
||||
width: rem-calc(8);
|
||||
margin-inline-end: space(2);
|
||||
margin-top: space(1.5);
|
||||
display: flex;
|
||||
flex: 0 0 rem-calc(8);
|
||||
}
|
||||
|
||||
.subHeadline {
|
||||
@include typoXS;
|
||||
@include margin(0, 0, 2);
|
||||
@include padding(0);
|
||||
text-transform: uppercase;
|
||||
opacity: 0.4;
|
||||
height: rem-calc(16);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex: 1 0 rem-calc(168);
|
||||
flex-direction: column;
|
||||
.titleContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
:first-child {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.titleText {
|
||||
justify-content: start !important;
|
||||
min-height: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.subTitleContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
:first-child {
|
||||
flex: auto;
|
||||
}
|
||||
.subTitleText {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
import { formatValue } from '../libs/parse'
|
||||
import styles from './CollectionHover.module.scss'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export interface HoverItem {
|
||||
color?: string
|
||||
name: string
|
||||
amount?: number
|
||||
usdValue?: number
|
||||
negative?: boolean
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data?: HoverItem[]
|
||||
title?: string
|
||||
noPercent?: boolean
|
||||
}
|
||||
|
||||
const CollectionHover = ({ data, title, noPercent }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
// -----------------
|
||||
// CALCULATE
|
||||
// -----------------
|
||||
const totalUsdValue = data
|
||||
? data.reduce((total, item) => total + (item.usdValue || 0), 0)
|
||||
: 0
|
||||
|
||||
const producePercentage = (fraction: number, sum: number): string => {
|
||||
if (fraction <= 0.01 || sum <= 0.01) {
|
||||
return '0.00%'
|
||||
} else {
|
||||
return formatValue((fraction / sum) * 100, 2, 2, true, false, '%')
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------
|
||||
// PRESENTATION
|
||||
// -----------------
|
||||
|
||||
const produceItem = (item: HoverItem, key: number) => {
|
||||
return (
|
||||
<div className={styles.item} key={key}>
|
||||
{item.usdValue ? (
|
||||
<div className={styles.valueItem}>
|
||||
{item.color && (
|
||||
<div
|
||||
className={styles.dot}
|
||||
style={{ backgroundColor: item.color }}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.content}>
|
||||
<div className={styles.titleContainer}>
|
||||
<span className={`body ${styles.titleText}`}>
|
||||
{item.name}
|
||||
</span>
|
||||
<span className={`body ${styles.titleText}`}>
|
||||
{item.amount
|
||||
? formatValue(item.amount)
|
||||
: formatValue(
|
||||
item.usdValue,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
item.negative ? '$-' : '$'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.subTitleContainer}>
|
||||
<span
|
||||
className={`caption ${styles.subTitleText}`}
|
||||
>
|
||||
{!noPercent &&
|
||||
producePercentage(
|
||||
item.usdValue,
|
||||
totalUsdValue
|
||||
)}
|
||||
</span>
|
||||
{item.amount && (
|
||||
<span
|
||||
className={`caption ${styles.subTitleText}`}
|
||||
>
|
||||
{formatValue(
|
||||
item.usdValue,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
item.negative ? '$-' : '$'
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.subHeadline}>{item.name}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return data ? (
|
||||
<div className={styles.container}>
|
||||
<p className='sub2'>{title ? title : t('common.summary')}</p>
|
||||
{data.map((item: HoverItem, index: number) =>
|
||||
produceItem(item, index)
|
||||
)}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
export default CollectionHover
|
@ -1,45 +0,0 @@
|
||||
import { useConnectedWallet, useWallet } from '@terra-money/wallet-provider'
|
||||
import { ReactNode, useEffect } from 'react'
|
||||
import networks from '../networks'
|
||||
import useBlockHeightQuery from '../queries-new/BlockHeightQuery'
|
||||
import useStore from '../store'
|
||||
|
||||
interface CommonContainerProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const CommonContainer = ({ children }: CommonContainerProps) => {
|
||||
/**
|
||||
* Network configurations
|
||||
*/
|
||||
const { network: extNetwork, status } = useWallet()
|
||||
const networkName = extNetwork.name
|
||||
const network = networks[networkName]
|
||||
const connectedWallet = useConnectedWallet()
|
||||
|
||||
const isNetworkLoaded = useStore((s) => s.isNetworkLoaded)
|
||||
const setNetworkInfo = useStore((s) => s.setNetworkInfo)
|
||||
const setUserWalletAddress = useStore((s) => s.setUserWalletAddress)
|
||||
const setNetworkConfig = useStore((s) => s.setNetworkConfig)
|
||||
|
||||
useEffect(() => {
|
||||
setNetworkConfig(network || extNetwork)
|
||||
}, [network, extNetwork, setNetworkConfig])
|
||||
|
||||
useEffect(() => {
|
||||
setUserWalletAddress(connectedWallet?.terraAddress ?? '')
|
||||
}, [setUserWalletAddress, connectedWallet])
|
||||
|
||||
useEffect(() => {
|
||||
setNetworkInfo(networkName, status)
|
||||
}, [networkName, status, setNetworkInfo])
|
||||
|
||||
/**
|
||||
* Blockchain meta data
|
||||
*/
|
||||
useBlockHeightQuery()
|
||||
|
||||
return <>{isNetworkLoaded && children}</>
|
||||
}
|
||||
|
||||
export default CommonContainer
|
@ -1,74 +0,0 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.network {
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 200;
|
||||
position: fixed;
|
||||
margin: -100% 0 0;
|
||||
transition: margin 0.5s;
|
||||
|
||||
&.show {
|
||||
@include margin(0);
|
||||
}
|
||||
|
||||
.container {
|
||||
@include margin(0);
|
||||
@include padding(6, 1);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
text-align: center;
|
||||
background-color: $colorAccent;
|
||||
}
|
||||
|
||||
.headline {
|
||||
width: 100%;
|
||||
display: block;
|
||||
@include typoScaps;
|
||||
@include margin(0, 0, 3);
|
||||
}
|
||||
|
||||
p {
|
||||
width: 100%;
|
||||
display: block;
|
||||
letter-spacing: rem-calc(1);
|
||||
@include typoXS;
|
||||
@include margin(0, 0, 3);
|
||||
}
|
||||
|
||||
.link {
|
||||
color: $colorPrimary;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
right: space(3);
|
||||
top: space(3);
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.5s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
background: transparent;
|
||||
@include padding(0);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: rem-calc(20);
|
||||
width: rem-calc(20);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
import { memo, useEffect, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import styles from './ErrorBanner.module.scss'
|
||||
import { CloseSVG } from './Svg'
|
||||
|
||||
interface ErrorBannerProps {
|
||||
hasNetworkError: boolean
|
||||
hasQueryError: boolean
|
||||
hasServerError: boolean
|
||||
isNetworkSupported: boolean | undefined
|
||||
}
|
||||
|
||||
const ErrorBanner = memo(
|
||||
({
|
||||
hasNetworkError,
|
||||
hasQueryError,
|
||||
hasServerError,
|
||||
isNetworkSupported,
|
||||
}: ErrorBannerProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [hideError, setHideError] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (hasNetworkError || hasQueryError || hasServerError) {
|
||||
setHideError(false)
|
||||
}
|
||||
}, [hasNetworkError, hasQueryError, hasServerError])
|
||||
|
||||
const getHeadline = (): string => {
|
||||
return hasNetworkError
|
||||
? t('common.appearToBeOffline')
|
||||
: hasServerError
|
||||
? t('error.serverOfflineTitle')
|
||||
: t('error.failingRequest')
|
||||
}
|
||||
|
||||
const getBody = (): string => {
|
||||
return hasNetworkError
|
||||
? t('error.youHaveAFailingNetworkRequest')
|
||||
: hasServerError
|
||||
? t('error.serverOfflineBody')
|
||||
: t('error.failingRequestDescription')
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Error banner is disabled for GQL errors currently */}
|
||||
{(hasNetworkError || hasServerError) && isNetworkSupported && (
|
||||
<div
|
||||
className={
|
||||
!hideError
|
||||
? `${styles.network} ${styles.show}`
|
||||
: styles.network
|
||||
}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.close}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setHideError(true)
|
||||
}}
|
||||
>
|
||||
<CloseSVG />
|
||||
</button>
|
||||
</div>
|
||||
<h3 className={styles.headline}>{getHeadline()}</h3>
|
||||
<p>{getBody()}</p>
|
||||
{!hasNetworkError && (
|
||||
<p>
|
||||
<Trans i18nKey={'error.problemPersists'}>
|
||||
text
|
||||
<a
|
||||
className={styles.link}
|
||||
href='https://discord.gg/marsprotocol'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
link
|
||||
</a>
|
||||
</Trans>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default ErrorBanner
|
@ -1,54 +0,0 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.select {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: $fontColorLightPrimary;
|
||||
width: rem-calc(120);
|
||||
height: rem-calc(37);
|
||||
display: inline-block;
|
||||
@include padding(0.5, 0, 0.5, 3);
|
||||
appearance: none;
|
||||
box-sizing: border-box;
|
||||
@include typoM;
|
||||
outline: none;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.select::-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.selectWrapper {
|
||||
border: 1px solid $buttonBorder;
|
||||
color: $fontColorLightPrimary;
|
||||
width: rem-calc(120);
|
||||
height: rem-calc(37);
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
border-radius: $borderRadiusXXS;
|
||||
display: flex;
|
||||
flex: 0 0 rem-calc(37);
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
width: rem-calc(12);
|
||||
height: rem-calc(8);
|
||||
background-color: $fontColorLightPrimary;
|
||||
clip-path: polygon(100% 0%, 0 0%, 50% 100%);
|
||||
justify-self: end;
|
||||
position: absolute;
|
||||
right: rem-calc(8);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
import i18n from 'i18next'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import styles from './LanguageSelect.module.scss'
|
||||
|
||||
const LanguageSelect = () => {
|
||||
const [currentLanguage, setCurrentLanguage] = useState('en')
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const lang = i18n.language.substring(0, 2) || 'en'
|
||||
if (currentLanguage !== lang) {
|
||||
setCurrentLanguage(lang)
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[i18n.language, currentLanguage]
|
||||
)
|
||||
|
||||
const changeLanguage = (lng: string) => {
|
||||
setCurrentLanguage(lng)
|
||||
i18n.changeLanguage(lng)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.selectWrapper}>
|
||||
<select
|
||||
className={styles.select}
|
||||
value={currentLanguage}
|
||||
onChange={(e) => changeLanguage(e.target.value)}
|
||||
>
|
||||
<option value='de'>Deutsch</option>
|
||||
<option value='en'>English</option>
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LanguageSelect
|
@ -1,65 +0,0 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.notification {
|
||||
width: 100%;
|
||||
min-height: rem-calc(48);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include layoutPopover;
|
||||
margin-bottom: space(8) !important;
|
||||
@include padding(2, 3);
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
|
||||
p {
|
||||
@include margin(0);
|
||||
font-weight: $fontWeightSemibold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.info {
|
||||
color: $colorAccent;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: $colorInfoWarning;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: $colorInfoWarning;
|
||||
}
|
||||
|
||||
&.closeBtnSpace {
|
||||
@include padding(0, 10, 0, 0);
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
@include margin(0, 1);
|
||||
color: $colorSecondary;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.closeNotification {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
@include margin(0, 4, 0, 0);
|
||||
border: none;
|
||||
background: transparent;
|
||||
@include padding(0);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: rem-calc(14);
|
||||
height: rem-calc(13);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
import { ReactNode, useMemo, useState } from 'react'
|
||||
|
||||
import styles from './Notification.module.scss'
|
||||
import { SmallCloseSVG } from './Svg'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { NotificationType } from '../types/enums'
|
||||
|
||||
interface Props {
|
||||
showNotification: boolean
|
||||
content: ReactNode
|
||||
type: NotificationType
|
||||
hideCloseBtn?: boolean
|
||||
}
|
||||
|
||||
const Notification = ({
|
||||
showNotification,
|
||||
content,
|
||||
type,
|
||||
hideCloseBtn = false,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [closeNotification, setCloseNotification] = useState(false)
|
||||
|
||||
const typeClass = useMemo(() => {
|
||||
return type === NotificationType.Warning
|
||||
? styles.warning
|
||||
: type === NotificationType.Error
|
||||
? styles.error
|
||||
: styles.info
|
||||
}, [type])
|
||||
|
||||
return (
|
||||
<>
|
||||
{showNotification && !closeNotification ? (
|
||||
<div
|
||||
className={`${styles.notification} ${typeClass} ${
|
||||
!hideCloseBtn ? styles.closeBtnSpace : null
|
||||
}`}
|
||||
>
|
||||
<p>{content}</p>
|
||||
|
||||
{!hideCloseBtn && (
|
||||
<button
|
||||
title={t('common.close')}
|
||||
className={styles.closeNotification}
|
||||
onClick={() => {
|
||||
setCloseNotification(true)
|
||||
}}
|
||||
>
|
||||
<SmallCloseSVG />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Notification
|
@ -1,78 +0,0 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.position {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
align-items: flex-start;
|
||||
flex-wrap: nowrap;
|
||||
min-height: rem-calc(61);
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: rem-calc(200);
|
||||
flex: 0 0 rem-calc(200);
|
||||
|
||||
.value {
|
||||
word-wrap: normal;
|
||||
word-break: normal;
|
||||
|
||||
h5 {
|
||||
span {
|
||||
@include typoM;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bar {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: flex-start;
|
||||
@include margin(2, 0, 0);
|
||||
height: rem-calc(8);
|
||||
flex-wrap: nowrap;
|
||||
|
||||
> .fraction {
|
||||
border-radius: $borderRadiusXXS;
|
||||
display: inline-block;
|
||||
@include margin(0, 0, 0, -1);
|
||||
@include padding(0, 0, 0, 1);
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
@include margin(0);
|
||||
@include padding(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.box {
|
||||
box-sizing: unset;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.compact {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpMediumHigh) {
|
||||
.compact {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpSmallHigh) {
|
||||
.position {
|
||||
.container {
|
||||
width: rem-calc(120);
|
||||
flex: 0 0 rem-calc(120);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
import Tippy from '@tippyjs/react'
|
||||
import styles from './PositionBar.module.scss'
|
||||
import colors from '../styles/_assets.module.scss'
|
||||
import { formatValue, lookup } from '../libs/parse'
|
||||
import CollectionHover, { HoverItem } from './CollectionHover'
|
||||
import { ReactElement } from 'react'
|
||||
import { UST_DECIMALS, UST_DENOM } from '../constants/appConstants'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
const PositionBar = ({
|
||||
title,
|
||||
value,
|
||||
bars,
|
||||
total,
|
||||
compactView = false,
|
||||
}: StrategyBarProps) => {
|
||||
const { t } = useTranslation()
|
||||
const produceData = (data: StrategyBarItem[]): HoverItem[] => {
|
||||
const items: HoverItem[] = []
|
||||
|
||||
data.forEach((asset: StrategyBarItem) => {
|
||||
if (asset.value !== 0) {
|
||||
items.push({
|
||||
color: asset.color || '',
|
||||
name: t(`strategy.${asset.name}`) || '',
|
||||
usdValue: lookup(asset.value, UST_DENOM, UST_DECIMALS),
|
||||
})
|
||||
}
|
||||
})
|
||||
return items
|
||||
}
|
||||
|
||||
const renderBars = (bars: StrategyBarItem[]): ReactElement[] => {
|
||||
const barParts: ReactElement[] = []
|
||||
bars.forEach((item: StrategyBarItem, index: number) => {
|
||||
barParts.push(
|
||||
<div
|
||||
key={index}
|
||||
className={styles.fraction}
|
||||
style={{
|
||||
width:
|
||||
item.value === 0
|
||||
? '0%'
|
||||
: ((item.value / total) * 100).toFixed(2) + '%',
|
||||
zIndex: 50 - index,
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
return barParts
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
compactView
|
||||
? `${styles.position} ${styles.compact}`
|
||||
: `${styles.position}`
|
||||
}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.value}>
|
||||
<h4>
|
||||
<span>$</span>
|
||||
{formatValue(value)}
|
||||
</h4>
|
||||
</div>
|
||||
<span className={'sub2'}>{title}</span>
|
||||
</div>
|
||||
{bars.length === 0 ? (
|
||||
<div className={styles.bar}>
|
||||
<div
|
||||
className={styles.fraction}
|
||||
style={{
|
||||
width: '33%',
|
||||
zIndex: 50 - 1,
|
||||
backgroundColor: colors.transparentWhite,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{!(bars.length === 1 && bars[0].value === 0) && (
|
||||
<Tippy
|
||||
className={styles.box}
|
||||
content={
|
||||
<CollectionHover
|
||||
title={title}
|
||||
data={produceData(bars)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={styles.bar}>{renderBars(bars)}</div>
|
||||
</Tippy>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PositionBar
|
File diff suppressed because one or more lines are too long
@ -1,17 +0,0 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
@include margin(8, 0);
|
||||
opacity: 0.6;
|
||||
|
||||
h6 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.horizontalLine {
|
||||
flex: auto;
|
||||
@include devider60;
|
||||
@include margin(0, 0, 3);
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import styles from './Title.module.scss'
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
margin?: string
|
||||
}
|
||||
|
||||
const Title = ({ text, margin }: Props) => {
|
||||
return (
|
||||
<div className={styles.title}>
|
||||
<div className={styles.horizontalLine} />
|
||||
<h6 style={{ margin: margin || '0 40px' }}>{text}</h6>
|
||||
<div className={styles.horizontalLine} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Title
|
@ -1,22 +0,0 @@
|
||||
@use '../styles/master' as *;
|
||||
|
||||
.txFee {
|
||||
@include padding(0, 1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.3;
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
@include margin(0, 1.5, 0, 0);
|
||||
}
|
||||
|
||||
svg {
|
||||
height: inherit;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
import styles from './TxFee.module.scss'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { formatValue } from '../libs/parse'
|
||||
|
||||
interface Props {
|
||||
txFee: string
|
||||
styleOverride?: any
|
||||
}
|
||||
|
||||
const TxFee = ({ txFee, styleOverride = {} }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className={styles.txFee}>
|
||||
<div className={`overline ${styles.label}`} style={styleOverride}>
|
||||
<span>{t('common.txFee')}</span>
|
||||
</div>
|
||||
<div className={`overline ${styles.value}`} style={styleOverride}>
|
||||
{formatValue(txFee, 2, 2, true, false, ' UST', true)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TxFee
|
@ -1,41 +0,0 @@
|
||||
import { formatValue } from '../libs/parse'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
gasFeeFormatted: string
|
||||
taxFormatted: string
|
||||
}
|
||||
|
||||
const TxFeeToolTip = ({ gasFeeFormatted, taxFormatted }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div style={{ fontSize: '0.8rem' }}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<span style={{ flex: 'auto', marginRight: '6px' }}>
|
||||
{t('common.gas')}
|
||||
</span>
|
||||
<span>
|
||||
{formatValue(
|
||||
gasFeeFormatted,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
false,
|
||||
' UST',
|
||||
true
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<span style={{ flex: 'auto', marginRight: '6px' }}>
|
||||
{t('common.stability')}
|
||||
</span>
|
||||
<span>
|
||||
{formatValue(taxFormatted, 2, 2, true, false, ' UST', true)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TxFeeToolTip
|
@ -1,113 +0,0 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
@include margin(12.5, 25, 4);
|
||||
width: rem-calc(230);
|
||||
height: rem-calc(265);
|
||||
border: 1px solid $graphAxis;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
|
||||
.scale {
|
||||
@include typoXXS;
|
||||
opacity: 0.6;
|
||||
line-height: rem-calc(12);
|
||||
position: absolute;
|
||||
width: rem-calc(95);
|
||||
text-align: end;
|
||||
left: rem-calc(-100);
|
||||
letter-spacing: rem-calc(2);
|
||||
|
||||
&.maxY {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&.minY {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.legend {
|
||||
@include typoXXScaps;
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
bottom: rem-calc(-20);
|
||||
|
||||
&.position {
|
||||
left: rem-calc(6);
|
||||
width: rem-calc(120);
|
||||
}
|
||||
|
||||
&.debt {
|
||||
right: rem-calc(-10);
|
||||
width: rem-calc(95);
|
||||
}
|
||||
}
|
||||
|
||||
.bar {
|
||||
max-height: 100%;
|
||||
width: rem-calc(36);
|
||||
border-radius: $borderRadiusXS;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
||||
&.supply {
|
||||
left: rem-calc(30);
|
||||
}
|
||||
|
||||
&.borrow {
|
||||
left: rem-calc(75);
|
||||
}
|
||||
|
||||
&.debt {
|
||||
left: rem-calc(172);
|
||||
transition: height 1s ease-out;
|
||||
}
|
||||
|
||||
.label {
|
||||
position: absolute;
|
||||
width: rem-calc(34);
|
||||
bottom: rem-calc(12);
|
||||
left: rem-calc(2);
|
||||
text-align: center;
|
||||
@include typoXXScaps;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.liquidation {
|
||||
@include typoXXScaps;
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
width: rem-calc(105);
|
||||
margin-bottom: space(-3);
|
||||
text-align: end;
|
||||
left: rem-calc(-110);
|
||||
transition: bottom 1s ease-out;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
width: 100%;
|
||||
word-wrap: normal;
|
||||
word-break: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.liquidationLine {
|
||||
display: block;
|
||||
width: rem-calc(110);
|
||||
height: 1px;
|
||||
border-bottom: 2px dashed $graphLiquidationsLine;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
transition: bottom 1s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $bpSmallHigh) {
|
||||
.container {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import styles from './BarGraph.module.scss'
|
||||
import { formatValue } from '../../libs/parse'
|
||||
|
||||
interface barGraphData {
|
||||
bars: number[]
|
||||
labels: string[]
|
||||
classNames: string[]
|
||||
range: number[]
|
||||
liquidation: number
|
||||
legend: string[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
data: barGraphData
|
||||
}
|
||||
|
||||
const BarGraph = ({ data }: Props) => {
|
||||
const liquidationPosition = data.liquidation / (data.range[1] / 100)
|
||||
|
||||
const getBarHeightPercentage = (barIndex: number): number => {
|
||||
return Math.floor(
|
||||
(data.bars[barIndex] /
|
||||
Math.max(
|
||||
data.bars[0] || 0,
|
||||
data.bars[1] || 0,
|
||||
data.bars[2] || 0
|
||||
)) *
|
||||
100
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<span className={`${styles.scale} ${styles.maxY}`}>
|
||||
{formatValue(data.range[1], 2, 2, true, '$')}
|
||||
</span>
|
||||
<span className={`${styles.scale} ${styles.minY}`}>
|
||||
{formatValue(data.range[0], 0, 0, true, '$')}
|
||||
</span>
|
||||
<span className={`${styles.legend} ${styles.position}`}>
|
||||
{data.legend[0]}
|
||||
</span>
|
||||
{data.bars[2] > 0 && (
|
||||
<span className={`${styles.legend} ${styles.debt}`}>
|
||||
{data.legend[1]}
|
||||
</span>
|
||||
)}
|
||||
{data.bars[0] > 0 && (
|
||||
<div
|
||||
className={`${styles.bar} ${styles.supply} ${data.classNames[0]}`}
|
||||
style={
|
||||
data.bars[0] === 0
|
||||
? { opacity: 0, height: '0%' }
|
||||
: { height: `${getBarHeightPercentage(0)}%` }
|
||||
}
|
||||
>
|
||||
<span className={styles.label}>{data.labels[0]}</span>
|
||||
</div>
|
||||
)}
|
||||
{data.bars[1] > 0 && (
|
||||
<div
|
||||
className={`${styles.bar} ${styles.borrow} ${data.classNames[1]}`}
|
||||
style={
|
||||
data.bars[1] === 0
|
||||
? { opacity: 0, height: '0%' }
|
||||
: { height: `${getBarHeightPercentage(1)}%` }
|
||||
}
|
||||
>
|
||||
<span className={styles.label}>{data.labels[1]}</span>
|
||||
</div>
|
||||
)}
|
||||
{data.bars[2] > 0 && (
|
||||
<div
|
||||
className={`${styles.bar} ${styles.debt} ${data.classNames[2]}`}
|
||||
style={
|
||||
data.bars[2] === 0
|
||||
? { height: '0%' }
|
||||
: { height: `${getBarHeightPercentage(2)}%` }
|
||||
}
|
||||
>
|
||||
<span className={styles.label}>{data.labels[2]}</span>
|
||||
</div>
|
||||
)}
|
||||
{data.liquidation > 0 && (
|
||||
<>
|
||||
<div
|
||||
className={styles.liquidation}
|
||||
style={{
|
||||
bottom: `${
|
||||
liquidationPosition < 13
|
||||
? 13
|
||||
: liquidationPosition
|
||||
}%`,
|
||||
}}
|
||||
>
|
||||
<span>Liquidation threshold</span>
|
||||
</div>
|
||||
<div
|
||||
className={styles.liquidationLine}
|
||||
style={{
|
||||
bottom: `${liquidationPosition}%`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BarGraph
|
@ -1,118 +0,0 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.title {
|
||||
@include margin(0, 0, 3);
|
||||
}
|
||||
|
||||
.progressbarContainer {
|
||||
position: relative;
|
||||
height: rem-calc(22);
|
||||
|
||||
.progressbar {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
@include bgHatched;
|
||||
box-shadow: $shadowInset;
|
||||
border-radius: $borderRadiusXXXL;
|
||||
}
|
||||
|
||||
.limitLine {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background: $colorInfoWarning;
|
||||
transition: left 2s;
|
||||
box-shadow: $shadowInset;
|
||||
}
|
||||
|
||||
.limit {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
background: $colorGreyDark;
|
||||
box-shadow: $shadowInset;
|
||||
border-top-left-radius: $borderRadiusXXXL;
|
||||
border-bottom-left-radius: $borderRadiusXXXL;
|
||||
transition: width 2s;
|
||||
}
|
||||
|
||||
.dot {
|
||||
position: absolute;
|
||||
width: rem-calc(3);
|
||||
height: rem-calc(3);
|
||||
background: $colorInfoWarning;
|
||||
margin-top: space(-1.75);
|
||||
margin-inline-start: space(-0.25);
|
||||
border-radius: $borderRadiusRound;
|
||||
transition: left 2s;
|
||||
}
|
||||
|
||||
.dotGlow {
|
||||
position: absolute;
|
||||
width: rem-calc(7);
|
||||
height: rem-calc(7);
|
||||
background: $colorInfoWarning;
|
||||
margin-top: space(-2.25);
|
||||
margin-inline-start: space(-1.25);
|
||||
border-radius: $borderRadiusXS;
|
||||
@include glowM;
|
||||
transition: left 2s;
|
||||
}
|
||||
|
||||
.ltvContainer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.mask {
|
||||
overflow-x: hidden;
|
||||
height: 100%;
|
||||
border-radius: $borderRadiusL;
|
||||
transition: width 2s;
|
||||
color: $fontColorLtv;
|
||||
|
||||
> span {
|
||||
transition: left 2s;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
@include bgLimit;
|
||||
}
|
||||
|
||||
.glow {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
@include bgLimit;
|
||||
opacity: 0.4;
|
||||
transition: width 2s;
|
||||
@include glowXXL;
|
||||
@include margin(-5.5, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.values {
|
||||
display: flex;
|
||||
opacity: 0.6;
|
||||
margin-top: space(1.25);
|
||||
|
||||
.zero {
|
||||
text-align: start;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.limit {
|
||||
flex: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,172 +0,0 @@
|
||||
import { formatValue, lookup } from '../../libs/parse'
|
||||
import styles from './BorrowLimit.module.scss'
|
||||
import { addDecimals } from '../../libs/math'
|
||||
import { UST_DECIMALS, UST_DENOM } from '../../constants/appConstants'
|
||||
|
||||
interface Props {
|
||||
width: string
|
||||
ltv: number
|
||||
maxLtv: number
|
||||
liquidationThreshold: number
|
||||
barHeight: string
|
||||
showPercentageText: boolean
|
||||
showTitleText: boolean
|
||||
showLegend?: boolean
|
||||
top?: number
|
||||
percentageThreshold?: number
|
||||
percentageOffset?: number
|
||||
title?: string
|
||||
mode?: string
|
||||
criticalIndicator?: number
|
||||
}
|
||||
|
||||
const BorrowLimit = ({
|
||||
width,
|
||||
ltv,
|
||||
maxLtv,
|
||||
liquidationThreshold,
|
||||
barHeight = '22px',
|
||||
showPercentageText = true,
|
||||
showLegend = true,
|
||||
top = 4,
|
||||
showTitleText = true,
|
||||
percentageThreshold = 15,
|
||||
percentageOffset = 45,
|
||||
title = 'Borrowing Capacity',
|
||||
mode = 'default',
|
||||
criticalIndicator,
|
||||
}: Props) => {
|
||||
const ltvPercent =
|
||||
+(((ltv || 0) / (liquidationThreshold || 0)) * 100).toFixed(2) || 0
|
||||
|
||||
const ltvPercentRounded = +(Math.round(ltvPercent * 100) / 100).toFixed(1)
|
||||
const ltvPercentRestrained =
|
||||
ltvPercent > 100 ? 100 : ltvPercent < 0 ? 0 : ltvPercent
|
||||
const ltvPercentMargin =
|
||||
ltvPercent > percentageThreshold
|
||||
? `-${percentageOffset + 15}px`
|
||||
: '10px'
|
||||
const maxBorrowPercent =
|
||||
criticalIndicator || (maxLtv / liquidationThreshold) * 100
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div style={{ width: width }}>
|
||||
{showTitleText ? (
|
||||
<div className={`overline ${styles.title}`}>{title}</div>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
style={{ height: barHeight }}
|
||||
className={styles.progressbarContainer}
|
||||
>
|
||||
<div
|
||||
style={{ width: width }}
|
||||
className={styles.progressbar}
|
||||
>
|
||||
<div
|
||||
style={{ left: `${maxBorrowPercent}%` }}
|
||||
className={styles.limitLine}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: `${maxBorrowPercent}%`,
|
||||
maxWidth: width,
|
||||
}}
|
||||
className={styles.limit}
|
||||
/>
|
||||
<div
|
||||
style={{ left: `${maxBorrowPercent}%` }}
|
||||
className={styles.dot}
|
||||
/>
|
||||
<div
|
||||
style={{ left: `${maxBorrowPercent}%` }}
|
||||
className={styles.dotGlow}
|
||||
/>
|
||||
|
||||
<div className={styles.ltvContainer}>
|
||||
<div
|
||||
style={{
|
||||
width: `${ltvPercentRestrained}%`,
|
||||
maxWidth: width,
|
||||
}}
|
||||
className={styles.mask}
|
||||
>
|
||||
{showPercentageText ? (
|
||||
<span
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${ltvPercentRestrained}%`,
|
||||
top: `${top}px`,
|
||||
marginLeft: ltvPercentMargin,
|
||||
width: `${percentageOffset + 8}px`,
|
||||
textAlign:
|
||||
ltvPercent > percentageThreshold
|
||||
? 'right'
|
||||
: 'left',
|
||||
}}
|
||||
className='overline'
|
||||
>
|
||||
{ltv < 0 ? (
|
||||
'0%'
|
||||
) : (
|
||||
<>
|
||||
{`${
|
||||
mode === 'default'
|
||||
? ltvPercentRounded
|
||||
: addDecimals(ltv)
|
||||
}%`}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
<div
|
||||
style={{ width: width }}
|
||||
className={styles.indicator}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: `${ltvPercentRestrained}%`,
|
||||
maxWidth: width,
|
||||
}}
|
||||
className={styles.glow}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{showLegend && (
|
||||
<div className={`overline ${styles.values}`}>
|
||||
<div className={styles.zero}>
|
||||
{mode === 'default' ? '$0' : '0%'}
|
||||
</div>
|
||||
<div className={styles.limit}>
|
||||
{mode === 'default' ? (
|
||||
<span>
|
||||
{formatValue(
|
||||
lookup(maxLtv, UST_DENOM, UST_DECIMALS),
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
'$'
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
formatValue(
|
||||
liquidationThreshold,
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
false,
|
||||
'%'
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BorrowLimit
|
@ -1,5 +0,0 @@
|
||||
@use '../../styles/master' as *;
|
||||
|
||||
.container {
|
||||
@include layoutTile;
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import { ReactNode } from 'react'
|
||||
import styles from './Card.module.scss'
|
||||
|
||||
interface Props {
|
||||
children?: ReactNode
|
||||
styleOverride?: object
|
||||
}
|
||||
|
||||
const Card = ({ children, styleOverride }: Props) => {
|
||||
return (
|
||||
<div style={styleOverride} className={styles.container}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Card
|
78
src/components/common/AnimatedNumber/AnimatedNumber.tsx
Normal file
78
src/components/common/AnimatedNumber/AnimatedNumber.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import classNames from 'classnames'
|
||||
import { formatValue } from 'libs/parse'
|
||||
import isEqual from 'lodash.isequal'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { animated, useSpring } from 'react-spring'
|
||||
import useStore from 'store'
|
||||
|
||||
interface AnimatedNumberProps {
|
||||
amount: number
|
||||
minDecimals?: number
|
||||
maxDecimals?: number
|
||||
thousandSeparator?: boolean
|
||||
prefix?: boolean | string
|
||||
suffix?: boolean | string
|
||||
rounded?: boolean
|
||||
abbreviated?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const AnimatedNumber = React.memo(
|
||||
({
|
||||
amount,
|
||||
minDecimals = 2,
|
||||
maxDecimals = 2,
|
||||
thousandSeparator = true,
|
||||
prefix = false,
|
||||
suffix = false,
|
||||
rounded = false,
|
||||
abbreviated = true,
|
||||
className,
|
||||
}: AnimatedNumberProps) => {
|
||||
const prevAmountRef = useRef<number>(0)
|
||||
const enableAnimations = useStore((s) => s.enableAnimations)
|
||||
|
||||
useEffect(() => {
|
||||
if (prevAmountRef.current !== amount) prevAmountRef.current = amount
|
||||
}, [amount])
|
||||
|
||||
const springAmount = useSpring({
|
||||
number: amount,
|
||||
from: { number: prevAmountRef.current },
|
||||
config: { duration: 1000 },
|
||||
})
|
||||
|
||||
return prevAmountRef.current === amount || !enableAnimations ? (
|
||||
<span className={classNames('number', className)}>
|
||||
{formatValue(
|
||||
amount,
|
||||
minDecimals,
|
||||
maxDecimals,
|
||||
thousandSeparator,
|
||||
prefix,
|
||||
suffix,
|
||||
rounded,
|
||||
abbreviated,
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<animated.span className={classNames('number', className)}>
|
||||
{springAmount.number.to((num) =>
|
||||
formatValue(
|
||||
num,
|
||||
minDecimals,
|
||||
maxDecimals,
|
||||
thousandSeparator,
|
||||
prefix,
|
||||
suffix,
|
||||
rounded,
|
||||
abbreviated,
|
||||
),
|
||||
)}
|
||||
</animated.span>
|
||||
)
|
||||
},
|
||||
(prevProps, nextProps) => isEqual(prevProps, nextProps),
|
||||
)
|
||||
|
||||
AnimatedNumber.displayName = 'AnimatedNumber'
|
22
src/components/common/Backdrop/Backdrop.module.scss
Normal file
22
src/components/common/Backdrop/Backdrop.module.scss
Normal file
@ -0,0 +1,22 @@
|
||||
@import 'src/styles/master';
|
||||
|
||||
.backdrop {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background: $alphaBlack20;
|
||||
opacity: 0;
|
||||
transition: opacity 200ms ease-in-out 100ms;
|
||||
|
||||
&.show {
|
||||
z-index: 20;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.hide {
|
||||
opacity: 0;
|
||||
display: none;
|
||||
}
|
||||
}
|
12
src/components/common/Backdrop/Backdrop.tsx
Normal file
12
src/components/common/Backdrop/Backdrop.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import classNames from 'classnames/bind'
|
||||
|
||||
import styles from './Backdrop.module.scss'
|
||||
|
||||
interface Props {
|
||||
show: boolean
|
||||
}
|
||||
|
||||
export const Backdrop = (props: Props) => {
|
||||
const classes = classNames(styles.backdrop, props.show ? styles.show : styles.hide)
|
||||
return <div className={classes} />
|
||||
}
|
21
src/components/common/BaseCurrency/BaseCurrency.tsx
Normal file
21
src/components/common/BaseCurrency/BaseCurrency.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { Coin } from '@cosmjs/stargate'
|
||||
import { formatValue } from 'libs/parse'
|
||||
import useStore from 'store'
|
||||
|
||||
interface Props {
|
||||
coins: Coin[]
|
||||
valueClass?: string
|
||||
}
|
||||
|
||||
export const BaseCurrency = ({ coins, valueClass }: Props) => {
|
||||
const baseCurrency = useStore((s) => s.baseCurrency)
|
||||
const convertToBaseCurrency = useStore((s) => s.convertToBaseCurrency)
|
||||
|
||||
const amount = coins.reduce((prev, curr) => prev + convertToBaseCurrency(curr), 0)
|
||||
|
||||
return (
|
||||
<p className={`${valueClass} number`}>
|
||||
{`${formatValue(amount / 10 ** baseCurrency.decimals, 2, 2, true)} ${baseCurrency.symbol}`}
|
||||
</p>
|
||||
)
|
||||
}
|
113
src/components/common/BorrowCapacity/BorrowCapacity.module.scss
Normal file
113
src/components/common/BorrowCapacity/BorrowCapacity.module.scss
Normal file
@ -0,0 +1,113 @@
|
||||
@import 'src/styles/master';
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.limitText {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
opacity: 0;
|
||||
transition: opacity 0.8s;
|
||||
transition-delay: 1.6s;
|
||||
|
||||
&.show {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progressbarContainer {
|
||||
position: relative;
|
||||
height: rem-calc(22);
|
||||
|
||||
.progressbar {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
@include bgHatched;
|
||||
box-shadow: $shadowInset;
|
||||
border-radius: $borderRadiusXXXL;
|
||||
}
|
||||
|
||||
.limitLine {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 120%;
|
||||
width: 1px;
|
||||
background: $colorWhite;
|
||||
transition: left $animationSpeed linear;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.limit {
|
||||
position: absolute;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
background: $colorGreyDark;
|
||||
box-shadow: $shadowInset;
|
||||
border-top-left-radius: $borderRadiusXXXL;
|
||||
border-bottom-left-radius: $borderRadiusXXXL;
|
||||
transition: width $animationSpeed linear;
|
||||
}
|
||||
|
||||
.limitBar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
background: $backgroundBodyDark;
|
||||
border-top-left-radius: $borderRadiusXXXL;
|
||||
border-bottom-left-radius: $borderRadiusXXXL;
|
||||
transition: right $animationSpeed linear;
|
||||
}
|
||||
|
||||
.ltvContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.indicator {
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
transition: width $animationSpeed linear;
|
||||
border-radius: $borderRadiusL;
|
||||
mask: linear-gradient(#fff 0 0);
|
||||
}
|
||||
|
||||
.percentage {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.gradient {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
@include bgLimit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.values {
|
||||
display: flex;
|
||||
opacity: 0.6;
|
||||
margin-top: space(2);
|
||||
|
||||
& > *:not(:last-child) {
|
||||
margin-right: space(1);
|
||||
}
|
||||
}
|
||||
}
|
172
src/components/common/BorrowCapacity/BorrowCapacity.tsx
Normal file
172
src/components/common/BorrowCapacity/BorrowCapacity.tsx
Normal file
@ -0,0 +1,172 @@
|
||||
import Tippy from '@tippyjs/react'
|
||||
import classNames from 'classnames'
|
||||
import { AnimatedNumber, DisplayCurrency } from 'components/common'
|
||||
import { formatValue } from 'libs/parse'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useStore from 'store'
|
||||
|
||||
import styles from './BorrowCapacity.module.scss'
|
||||
|
||||
interface Props {
|
||||
balance: number
|
||||
limit: number
|
||||
max: number
|
||||
barHeight: string
|
||||
showTitle?: boolean
|
||||
showPercentageText: boolean
|
||||
className?: string
|
||||
hideValues?: boolean
|
||||
roundPercentage?: boolean
|
||||
fadeTitle?: boolean
|
||||
}
|
||||
|
||||
export const BorrowCapacity = ({
|
||||
balance,
|
||||
limit,
|
||||
max,
|
||||
barHeight = '22px',
|
||||
showTitle = true,
|
||||
showPercentageText = true,
|
||||
className,
|
||||
hideValues,
|
||||
roundPercentage = false,
|
||||
fadeTitle,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const baseCurrency = useStore((s) => s.baseCurrency)
|
||||
const convertToDisplayCurrency = useStore((s) => s.convertToDisplayCurrency)
|
||||
|
||||
const [percentOfMaxRound, setPercentOfMaxRound] = useState(0)
|
||||
const [percentOfMaxRange, setPercentOfMaxRange] = useState(0)
|
||||
const [limitPercentOfMax, setLimitPercentOfMax] = useState(0)
|
||||
|
||||
useMemo(() => {
|
||||
if (max === 0) {
|
||||
setPercentOfMaxRound(0)
|
||||
setPercentOfMaxRange(0)
|
||||
setLimitPercentOfMax(0)
|
||||
return
|
||||
}
|
||||
|
||||
const pOfMax = +((balance / max) * 100).toFixed(2)
|
||||
setPercentOfMaxRound(+(Math.round(pOfMax * 100) / 100).toFixed(1))
|
||||
setPercentOfMaxRange(Math.min(Math.max(pOfMax, 0), 100))
|
||||
setLimitPercentOfMax((limit / max) * 100)
|
||||
}, [limit, balance, max])
|
||||
|
||||
return (
|
||||
<div className={`${styles.container} ${className}`}>
|
||||
<div style={{ width: '100%' }}>
|
||||
<div
|
||||
className={styles.header}
|
||||
style={{
|
||||
width: `${limitPercentOfMax ? limitPercentOfMax + 6 : '100'}%`,
|
||||
marginBottom: !showTitle && hideValues ? 0 : 12,
|
||||
}}
|
||||
>
|
||||
<div className={classNames('overline', fadeTitle && 'faded')}>
|
||||
{showTitle && t('common.borrowingCapacity')}
|
||||
</div>
|
||||
{!hideValues && (
|
||||
<DisplayCurrency
|
||||
className={classNames(
|
||||
styles.limitText,
|
||||
'overline xxxsCaps',
|
||||
limitPercentOfMax && styles.show,
|
||||
)}
|
||||
coin={{
|
||||
amount: limit.toString(),
|
||||
denom: baseCurrency.denom,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Tippy
|
||||
appendTo={() => document.body}
|
||||
interactive={true}
|
||||
animation={false}
|
||||
render={(attrs) => {
|
||||
return (
|
||||
<div className='tippyContainer' {...attrs}>
|
||||
{t('redbank.borrowingBarTooltip', {
|
||||
limit: formatValue(
|
||||
convertToDisplayCurrency({
|
||||
amount: limit.toString(),
|
||||
denom: baseCurrency.denom,
|
||||
}),
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
'$',
|
||||
),
|
||||
liquidation: formatValue(
|
||||
convertToDisplayCurrency({
|
||||
amount: max.toString(),
|
||||
denom: baseCurrency.denom,
|
||||
}),
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
'$',
|
||||
),
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
>
|
||||
<div className={styles.progressbarContainer} style={{ height: barHeight }}>
|
||||
<div className={styles.progressbar}>
|
||||
<div className={styles.limitLine} style={{ left: `${limitPercentOfMax || 0}%` }} />
|
||||
<div
|
||||
className={styles.limitBar}
|
||||
style={{
|
||||
right: `${limitPercentOfMax ? 100 - limitPercentOfMax : 100}%`,
|
||||
}}
|
||||
></div>
|
||||
|
||||
<div className={styles.ltvContainer}>
|
||||
<div
|
||||
className={styles.indicator}
|
||||
style={{ width: `${percentOfMaxRange || 0.01}%` }}
|
||||
>
|
||||
<div className={styles.gradient} />
|
||||
</div>
|
||||
{showPercentageText ? (
|
||||
<span className={classNames('overline', styles.percentage)}>
|
||||
{max !== 0 && (
|
||||
<AnimatedNumber
|
||||
amount={percentOfMaxRound}
|
||||
suffix='%'
|
||||
maxDecimals={roundPercentage ? 0 : undefined}
|
||||
minDecimals={roundPercentage ? 0 : undefined}
|
||||
abbreviated={false}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tippy>
|
||||
{!hideValues && (
|
||||
<div className={`overline ${styles.values} xxxsCaps`}>
|
||||
<DisplayCurrency
|
||||
coin={{
|
||||
amount: balance.toString(),
|
||||
denom: baseCurrency.denom,
|
||||
}}
|
||||
/>
|
||||
<span>{` ${t('common.of')} `}</span>
|
||||
<DisplayCurrency
|
||||
coin={{
|
||||
amount: max.toString(),
|
||||
denom: baseCurrency.denom,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
203
src/components/common/Button/Button.module.scss
Normal file
203
src/components/common/Button/Button.module.scss
Normal file
@ -0,0 +1,203 @@
|
||||
@import 'src/styles/master';
|
||||
|
||||
.button {
|
||||
transition: background-color 0.2s, border 0.2s;
|
||||
color: $fontColorLightPrimary;
|
||||
appearance: none;
|
||||
border: none;
|
||||
border-radius: $borderRadiusXXXL;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
align-items: center;
|
||||
word-wrap: normal;
|
||||
word-break: normal;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
|
||||
.prefix,
|
||||
.suffix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// Sizes
|
||||
&.small {
|
||||
@include buttonS;
|
||||
|
||||
svg,
|
||||
.progressIndicator {
|
||||
width: rem-calc(10);
|
||||
height: rem-calc(10);
|
||||
}
|
||||
|
||||
.prefix {
|
||||
margin-inline-end: space(2);
|
||||
}
|
||||
|
||||
.suffix {
|
||||
margin-inline-start: space(2);
|
||||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
@include buttonM;
|
||||
|
||||
svg {
|
||||
width: rem-calc(12);
|
||||
height: rem-calc(12);
|
||||
}
|
||||
|
||||
.prefix {
|
||||
margin-inline-end: space(3);
|
||||
}
|
||||
|
||||
.suffix {
|
||||
width: rem-calc(12);
|
||||
margin-inline-start: space(3);
|
||||
}
|
||||
}
|
||||
|
||||
&.large {
|
||||
@include buttonL;
|
||||
svg {
|
||||
width: rem-calc(18);
|
||||
height: rem-calc(18);
|
||||
}
|
||||
|
||||
.prefix {
|
||||
margin-inline-end: space(4.5);
|
||||
}
|
||||
|
||||
.suffix {
|
||||
margin-inline-start: space(4.5);
|
||||
}
|
||||
}
|
||||
|
||||
// Variants
|
||||
&.solid,
|
||||
&.round {
|
||||
@include buttonSolidPrimary;
|
||||
@include buttonSolidSecondary;
|
||||
@include buttonSolidTertiary;
|
||||
}
|
||||
|
||||
&.round {
|
||||
border-radius: $borderRadiusRound;
|
||||
@include padding(0);
|
||||
|
||||
.prefix,
|
||||
.suffix {
|
||||
@include margin(0);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
&.small {
|
||||
height: rem-calc(34);
|
||||
width: rem-calc(34);
|
||||
|
||||
svg {
|
||||
width: rem-calc(12);
|
||||
height: rem-calc(12);
|
||||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
height: rem-calc(40);
|
||||
width: rem-calc(40);
|
||||
|
||||
svg {
|
||||
width: rem-calc(14);
|
||||
height: rem-calc(14);
|
||||
}
|
||||
}
|
||||
|
||||
&.large {
|
||||
height: rem-calc(56);
|
||||
width: rem-calc(56);
|
||||
|
||||
svg {
|
||||
width: rem-calc(20);
|
||||
height: rem-calc(20);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.transparent {
|
||||
background: none;
|
||||
@include padding(0);
|
||||
height: unset;
|
||||
|
||||
&.primary {
|
||||
color: $colorPrimary;
|
||||
|
||||
* {
|
||||
color: $colorPrimary;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $colorPrimaryHighlight;
|
||||
|
||||
* {
|
||||
color: $colorPrimaryHighlight;
|
||||
}
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
color: $colorPrimaryHighlight;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
color: $colorSecondary;
|
||||
|
||||
* {
|
||||
color: $colorSecondary;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
color: $colorSecondaryHighlight;
|
||||
}
|
||||
}
|
||||
|
||||
&.tertiary {
|
||||
color: $colorSecondaryDark;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
color: lighten($colorSecondaryDark, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.quaternary {
|
||||
color: $alphaWhite60;
|
||||
border: 1px solid transparent;
|
||||
background: none;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
color: $colorWhite;
|
||||
border: 1px solid white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5 !important;
|
||||
}
|
91
src/components/common/Button/Button.tsx
Normal file
91
src/components/common/Button/Button.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import classNames from 'classnames'
|
||||
import { CircularProgress } from 'components/common'
|
||||
import React from 'react'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
import styles from './Button.module.scss'
|
||||
|
||||
interface Props {
|
||||
className?: any
|
||||
color?: 'primary' | 'secondary' | 'tertiary' | 'quaternary'
|
||||
disabled?: boolean
|
||||
externalLink?: string
|
||||
id?: string
|
||||
suffix?: ReactNode
|
||||
prefix?: ReactNode
|
||||
showProgressIndicator?: boolean
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
styleOverride?: ButtonStyleOverride
|
||||
text?: string | ReactNode
|
||||
variant?: 'solid' | 'transparent' | 'round'
|
||||
onClick?: (e: any) => void
|
||||
}
|
||||
|
||||
export const Button = React.forwardRef<any, Props>(
|
||||
(
|
||||
{
|
||||
className = '',
|
||||
color = 'primary',
|
||||
disabled,
|
||||
externalLink,
|
||||
id = '',
|
||||
suffix,
|
||||
prefix,
|
||||
showProgressIndicator,
|
||||
size = 'small',
|
||||
styleOverride,
|
||||
text,
|
||||
variant = 'solid',
|
||||
onClick,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const Button = () => {
|
||||
const buttonClasses = classNames(
|
||||
styles.button,
|
||||
styles[size],
|
||||
styles[color],
|
||||
styles[variant],
|
||||
disabled && styles.disabled,
|
||||
className,
|
||||
)
|
||||
return (
|
||||
<button
|
||||
className={buttonClasses}
|
||||
id={id}
|
||||
onClick={disabled ? () => {} : onClick}
|
||||
ref={ref}
|
||||
style={styleOverride}
|
||||
>
|
||||
{prefix && !showProgressIndicator && <span className={styles.prefix}>{prefix}</span>}
|
||||
{text && (
|
||||
<span className={styles.text}>
|
||||
{showProgressIndicator ? (
|
||||
<CircularProgress size={size === 'small' ? 10 : size === 'medium' ? 12 : 18} />
|
||||
) : (
|
||||
text
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{suffix && !showProgressIndicator && <span className={styles.suffix}>{suffix}</span>}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
return externalLink ? (
|
||||
<a
|
||||
className={styles.link}
|
||||
href={externalLink}
|
||||
ref={ref}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
{Button()}
|
||||
</a>
|
||||
) : (
|
||||
Button()
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
Button.displayName = 'Button'
|
83
src/components/common/Card/Card.module.scss
Normal file
83
src/components/common/Card/Card.module.scss
Normal file
@ -0,0 +1,83 @@
|
||||
@import 'src/styles/master';
|
||||
|
||||
.container {
|
||||
@include layoutTile;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
.header {
|
||||
@include padding(4, 10);
|
||||
min-height: rem-calc(80);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
position: relative;
|
||||
@include typoXXL;
|
||||
|
||||
h6 {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.button {
|
||||
color: $alphaWhite60;
|
||||
background: none;
|
||||
border: none;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: space(3);
|
||||
right: space(3);
|
||||
transition: color linear 0.2s;
|
||||
&:hover {
|
||||
color: $colorWhite;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: rem-calc(24);
|
||||
height: rem-calc(24);
|
||||
}
|
||||
|
||||
&.back {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: unset;
|
||||
left: space(4);
|
||||
|
||||
svg {
|
||||
width: rem-calc(32);
|
||||
height: rem-calc(32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.border {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: $bpXSmallLow) {
|
||||
.container {
|
||||
.header {
|
||||
.actions {
|
||||
display: block;
|
||||
margin-top: space(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: $bpLargeLow) {
|
||||
.container {
|
||||
.header {
|
||||
.actions {
|
||||
position: absolute;
|
||||
display: block;
|
||||
right: space(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
47
src/components/common/Card/Card.tsx
Normal file
47
src/components/common/Card/Card.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import classNames from 'classnames'
|
||||
import { SVG, Tooltip } from 'components/common'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
import styles from './Card.module.scss'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
tooltip?: string | ReactNode
|
||||
hideHeaderBorder?: boolean
|
||||
children?: ReactNode
|
||||
styleOverride?: object
|
||||
isClose?: boolean
|
||||
className?: string
|
||||
actionButtons?: ReactNode
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
export const Card = ({
|
||||
title,
|
||||
tooltip,
|
||||
hideHeaderBorder = false,
|
||||
children,
|
||||
styleOverride,
|
||||
isClose = false,
|
||||
className,
|
||||
actionButtons,
|
||||
onClick,
|
||||
}: Props) => {
|
||||
const headerClasses = classNames(styles.header, !hideHeaderBorder && styles.border)
|
||||
const buttonClasses = classNames(styles.button, !isClose && styles.back)
|
||||
return (
|
||||
<div className={`${styles.container} ${className}`} style={styleOverride}>
|
||||
<div className={headerClasses}>
|
||||
{onClick && (
|
||||
<button className={buttonClasses} onClick={onClick}>
|
||||
{isClose ? <SVG.Close /> : <SVG.ArrowBack />}
|
||||
</button>
|
||||
)}
|
||||
<h6>{title}</h6>
|
||||
{actionButtons && <div className={styles.actions}>{actionButtons}</div>}
|
||||
{tooltip && <Tooltip content={tooltip} />}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
11
src/components/common/CellAmount/CellAmount.module.scss
Normal file
11
src/components/common/CellAmount/CellAmount.module.scss
Normal file
@ -0,0 +1,11 @@
|
||||
@import 'src/styles/master';
|
||||
|
||||
.noBalanceText {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 460px) {
|
||||
.noBalanceText {
|
||||
display: block;
|
||||
}
|
||||
}
|
40
src/components/common/CellAmount/CellAmount.tsx
Normal file
40
src/components/common/CellAmount/CellAmount.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import classNames from 'classnames/bind'
|
||||
import { AnimatedNumber, DisplayCurrency } from 'components/common'
|
||||
import { lookup } from 'libs/parse'
|
||||
|
||||
import styles from './CellAmount.module.scss'
|
||||
|
||||
interface Props {
|
||||
denom: string
|
||||
decimals: number
|
||||
amount: number
|
||||
noBalanceText?: string
|
||||
}
|
||||
|
||||
export const CellAmount = ({ denom, decimals, amount, noBalanceText }: Props) => {
|
||||
const assetAmount = lookup(amount, denom, decimals)
|
||||
const noBalanceClasses = classNames('s', 'faded', styles.noBalanceText)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p className='m'>
|
||||
<AnimatedNumber
|
||||
amount={assetAmount > 0 && assetAmount < 0.01 ? 0.01 : assetAmount}
|
||||
suffix={assetAmount > 0 && assetAmount < 0.01 ? '< ' : false}
|
||||
/>
|
||||
</p>
|
||||
{amount === 0 ? (
|
||||
<p className={noBalanceClasses}>{noBalanceText}</p>
|
||||
) : (
|
||||
<DisplayCurrency
|
||||
coin={{
|
||||
amount: amount.toString(),
|
||||
denom,
|
||||
}}
|
||||
prefixClass='s faded'
|
||||
valueClass='s faded'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
36
src/components/common/Checkbox/Checkbox.module.scss
Normal file
36
src/components/common/Checkbox/Checkbox.module.scss
Normal file
@ -0,0 +1,36 @@
|
||||
@import 'src/styles/master';
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
.checkbox {
|
||||
appearance: none;
|
||||
margin-right: space(3);
|
||||
cursor: pointer;
|
||||
|
||||
font: inherit;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 1.5px solid $alphaWhite40;
|
||||
border-radius: $borderRadiusXS;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
clip-path: polygon(14% 44%, 0% 65%, 40% 95%, 100% 26%, 85% 10%, 38% 62%);
|
||||
transform: scale(0);
|
||||
transform-origin: center center;
|
||||
transition: 100ms transform ease-in-out;
|
||||
box-shadow: inset 1rem 1rem $colorWhite;
|
||||
}
|
||||
|
||||
&:checked::before {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
24
src/components/common/Checkbox/Checkbox.tsx
Normal file
24
src/components/common/Checkbox/Checkbox.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import styles from './Checkbox.module.scss'
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
className?: string
|
||||
onChecked: (isChecked: boolean) => void
|
||||
}
|
||||
|
||||
export const Checkbox = (props: Props) => {
|
||||
const [isChecked, setIsChecked] = useState(false)
|
||||
|
||||
const handleChange = () => {
|
||||
setIsChecked(!isChecked)
|
||||
props.onChecked(!isChecked)
|
||||
}
|
||||
return (
|
||||
<label className={`${props.className} ${styles.container}`}>
|
||||
<input type='checkbox' onChange={handleChange} className={styles.checkbox} />
|
||||
{props.text}
|
||||
</label>
|
||||
)
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
@import 'src/styles/master';
|
||||
|
||||
.loader {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
|
||||
.element {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 80%;
|
||||
height: 80%;
|
||||
margin: 10%;
|
||||
border-radius: 50%;
|
||||
border-style: solid;
|
||||
animation: circularProgress 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
|
||||
&:nth-child(1) {
|
||||
animation-delay: -0.45s;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
&:nth-child(3) {
|
||||
animation-delay: -0.15s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes circularProgress {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
52
src/components/common/CircularProgress/CircularProgress.tsx
Normal file
52
src/components/common/CircularProgress/CircularProgress.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import classNames from 'classnames'
|
||||
import useStore from 'store'
|
||||
|
||||
import styles from './CircularProgress.module.scss'
|
||||
|
||||
interface Props {
|
||||
color?: string
|
||||
size?: number
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const CircularProgress = ({ color = '#FFFFFF', size = 20, className }: Props) => {
|
||||
const enableAnimations = useStore((s) => s.enableAnimations)
|
||||
const borderWidth = `${size / 10}px`
|
||||
const borderColor = `${color} transparent transparent transparent`
|
||||
const loaderClasses = classNames(styles.loader, className)
|
||||
|
||||
if (!enableAnimations) return <div className={styles.staticLoader}>...</div>
|
||||
|
||||
return (
|
||||
<div className={loaderClasses} style={{ width: `${size}px`, height: `${size}px` }}>
|
||||
<div
|
||||
className={styles.element}
|
||||
style={{
|
||||
borderWidth: borderWidth,
|
||||
borderColor: borderColor,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={styles.element}
|
||||
style={{
|
||||
borderWidth: borderWidth,
|
||||
borderColor: borderColor,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={styles.element}
|
||||
style={{
|
||||
borderWidth: borderWidth,
|
||||
borderColor: borderColor,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={styles.element}
|
||||
style={{
|
||||
borderWidth: borderWidth,
|
||||
borderColor: borderColor,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
@import 'src/styles/master';
|
||||
|
||||
.container {
|
||||
@include layoutTooltip;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: rem-calc(264);
|
||||
box-sizing: unset;
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
|
||||
.valueItem {
|
||||
margin-top: space(1);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.dot {
|
||||
border: 1px solid $colorWhite;
|
||||
height: rem-calc(8);
|
||||
border-radius: $borderRadiusRound;
|
||||
width: rem-calc(8);
|
||||
margin-inline-end: space(2);
|
||||
margin-top: space(1.5);
|
||||
display: flex;
|
||||
flex: 0 0 rem-calc(8);
|
||||
}
|
||||
|
||||
.subHeadline {
|
||||
@include typoXS;
|
||||
@include margin(0, 0, 2);
|
||||
@include padding(0);
|
||||
text-transform: uppercase;
|
||||
opacity: 0.4;
|
||||
height: rem-calc(16);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex: 1 0 rem-calc(168);
|
||||
flex-direction: column;
|
||||
.titleContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
:first-child {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.titleText {
|
||||
justify-content: start !important;
|
||||
min-height: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.subTitleContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
:first-child {
|
||||
flex: auto;
|
||||
}
|
||||
.subTitleText {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
65
src/components/common/CollectionHover/CollectionHover.tsx
Normal file
65
src/components/common/CollectionHover/CollectionHover.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { DisplayCurrency } from 'components/common'
|
||||
import { formatValue } from 'libs/parse'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import styles from './CollectionHover.module.scss'
|
||||
|
||||
interface Props {
|
||||
data?: HoverItem[]
|
||||
title?: string
|
||||
noPercent?: boolean
|
||||
}
|
||||
|
||||
export const CollectionHover = ({ data, title, noPercent }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
// -----------------
|
||||
// CALCULATE
|
||||
// -----------------
|
||||
const totalValue = data
|
||||
? data.reduce((total, item) => total + (Number(item.coin.amount) || 0), 0)
|
||||
: 0
|
||||
|
||||
const producePercentage = (fraction: number, sum: number): string => {
|
||||
if (fraction <= 0.01 || sum <= 0.01) {
|
||||
return '0.00%'
|
||||
} else {
|
||||
return formatValue((fraction / sum) * 100, 2, 2, true, false, '%')
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------
|
||||
// PRESENTATION
|
||||
// -----------------
|
||||
const produceItem = (item: HoverItem, key: number) => {
|
||||
return (
|
||||
<div className={styles.item} key={key}>
|
||||
{item.coin.amount ? (
|
||||
<div className={styles.valueItem}>
|
||||
{item.color && <div className={styles.dot} style={{ backgroundColor: item.color }} />}
|
||||
<div className={styles.content}>
|
||||
<div className={styles.titleContainer}>
|
||||
<span className={`body ${styles.titleText}`}>{item.name}</span>
|
||||
<DisplayCurrency className={`body ${styles.titleText}`} coin={item.coin} />
|
||||
</div>
|
||||
|
||||
<div className={styles.subTitleContainer}>
|
||||
<span className={`caption number ${styles.subTitleText}`}>
|
||||
{!noPercent && producePercentage(Number(item.coin.amount), totalValue)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.subHeadline}>{item.name}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return data ? (
|
||||
<div className={styles.container}>
|
||||
<p className='sub2'>{title ? title : t('common.summary')}</p>
|
||||
{data.map((item: HoverItem, index: number) => produceItem(item, index))}
|
||||
</div>
|
||||
) : null
|
||||
}
|
138
src/components/common/Containers/CommonContainer.tsx
Normal file
138
src/components/common/Containers/CommonContainer.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
import { useWallet } from '@marsprotocol/wallet-connector'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { MARS_SYMBOL, USDC_SYMBOL } from 'constants/appConstants'
|
||||
import {
|
||||
useBlockHeight,
|
||||
useMarketDeposits,
|
||||
useMarsOracle,
|
||||
useRedBank,
|
||||
useUserBalance,
|
||||
useUserDebt,
|
||||
useUserDeposit,
|
||||
} from 'hooks/queries'
|
||||
import { useSpotPrice } from 'hooks/queries/useSpotPrice'
|
||||
import { ReactNode, useEffect } from 'react'
|
||||
import useStore from 'store'
|
||||
import { State } from 'types/enums'
|
||||
import { Network } from 'types/enums/network'
|
||||
|
||||
interface CommonContainerProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const CommonContainer = ({ children }: CommonContainerProps) => {
|
||||
// ------------------
|
||||
// EXTERNAL HOOKS
|
||||
// ---------------
|
||||
const { chainInfo, address, signingCosmWasmClient, name } = useWallet()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// ------------------
|
||||
// STORE STATE
|
||||
// ------------------
|
||||
const chainID = useStore((s) => s.chainInfo?.chainId)
|
||||
const exchangeRates = useStore((s) => s.exchangeRates)
|
||||
const exchangeRatesState = useStore((s) => s.exchangeRatesState)
|
||||
const isNetworkLoaded = useStore((s) => s.isNetworkLoaded)
|
||||
const rpc = useStore((s) => s.chainInfo?.rpc)
|
||||
const marketAssetLiquidity = useStore((s) => s.marketAssetLiquidity)
|
||||
const marketDeposits = useStore((s) => s.marketDeposits)
|
||||
const marketInfo = useStore((s) => s.marketInfo)
|
||||
const marketIncentiveInfo = useStore((s) => s.marketIncentiveInfo)
|
||||
const redBankState = useStore((s) => s.redBankState)
|
||||
const userBalances = useStore((s) => s.userBalances)
|
||||
const userBalancesState = useStore((s) => s.userBalancesState)
|
||||
const userDebts = useStore((s) => s.userDebts)
|
||||
const userDeposits = useStore((s) => s.userDeposits)
|
||||
const userWalletAddress = useStore((s) => s.userWalletAddress)
|
||||
const whitelistedAssets = useStore((s) => s.whitelistedAssets)
|
||||
const loadNetworkConfig = useStore((s) => s.loadNetworkConfig)
|
||||
const setRedBankAssets = useStore((s) => s.setRedBankAssets)
|
||||
const setChainInfo = useStore((s) => s.setChainInfo)
|
||||
const setCurrentNetwork = useStore((s) => s.setCurrentNetwork)
|
||||
const setLcdClient = useStore((s) => s.setLcdClient)
|
||||
const setClient = useStore((s) => s.setClient)
|
||||
const setUserBalancesState = useStore((s) => s.setUserBalancesState)
|
||||
const setUserWalletAddress = useStore((s) => s.setUserWalletAddress)
|
||||
const setUserWalletName = useStore((s) => s.setUserWalletName)
|
||||
|
||||
// ------------------
|
||||
// SETTERS
|
||||
// ------------------
|
||||
useEffect(() => {
|
||||
if (process.env.NEXT_PUBLIC_NETWORK === 'mainnet') {
|
||||
setCurrentNetwork(Network.MAINNET)
|
||||
}
|
||||
loadNetworkConfig()
|
||||
}, [loadNetworkConfig, setCurrentNetwork])
|
||||
|
||||
useEffect(() => {
|
||||
if (!chainInfo) return
|
||||
setChainInfo(chainInfo)
|
||||
}, [chainInfo, setChainInfo])
|
||||
|
||||
useEffect(() => {
|
||||
setUserWalletAddress(address || '')
|
||||
}, [setUserWalletAddress, address])
|
||||
|
||||
useEffect(() => {
|
||||
if (!name) return
|
||||
setUserWalletName(name)
|
||||
}, [setUserWalletName, name])
|
||||
|
||||
useEffect(() => {
|
||||
if (!rpc || !chainID) return
|
||||
setLcdClient(rpc, chainID)
|
||||
}, [rpc, chainID, setLcdClient])
|
||||
|
||||
useEffect(() => {
|
||||
if (!signingCosmWasmClient) return
|
||||
setClient(signingCosmWasmClient)
|
||||
}, [signingCosmWasmClient, setClient])
|
||||
|
||||
useEffect(() => {
|
||||
if (userDebts && userDeposits && userBalances) {
|
||||
setUserBalancesState(State.READY)
|
||||
} else {
|
||||
setUserBalancesState(State.ERROR)
|
||||
}
|
||||
}, [userDebts, userDeposits, userBalances, setUserBalancesState])
|
||||
|
||||
useEffect(() => {
|
||||
setRedBankAssets()
|
||||
}, [
|
||||
exchangeRatesState,
|
||||
redBankState,
|
||||
userBalancesState,
|
||||
exchangeRates,
|
||||
marketInfo,
|
||||
marketAssetLiquidity,
|
||||
marketIncentiveInfo,
|
||||
userDebts,
|
||||
userDeposits,
|
||||
whitelistedAssets,
|
||||
marketDeposits,
|
||||
setRedBankAssets,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
queryClient.removeQueries()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [userWalletAddress])
|
||||
|
||||
// ------------------
|
||||
// QUERY RELATED
|
||||
// ------------------
|
||||
useBlockHeight()
|
||||
useRedBank()
|
||||
useUserBalance()
|
||||
useUserDeposit()
|
||||
useUserDebt()
|
||||
useMarsOracle()
|
||||
useSpotPrice(MARS_SYMBOL)
|
||||
useSpotPrice(USDC_SYMBOL)
|
||||
useMarketDeposits()
|
||||
useRedBank()
|
||||
|
||||
return <>{isNetworkLoaded && children}</>
|
||||
}
|
28
src/components/common/Containers/FieldsContainer.tsx
Normal file
28
src/components/common/Containers/FieldsContainer.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import React, { ReactNode, useEffect } from 'react'
|
||||
import useStore from 'store'
|
||||
|
||||
interface FieldsContainerProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const FieldsContainer = ({ children }: FieldsContainerProps) => {
|
||||
const client = useStore((s) => s.client)
|
||||
const networkConfig = useStore((s) => s.networkConfig)
|
||||
const userWalletAddress = useStore((s) => s.userWalletAddress)
|
||||
const setAccountNftClient = useStore((s) => s.setAccountNftClient)
|
||||
const setCreditManagerClient = useStore((s) => s.setCreditManagerClient)
|
||||
const setCreditManagerMsgComposer = useStore((s) => s.setCreditManagerMsgComposer)
|
||||
|
||||
useEffect(() => {
|
||||
if (!client) return
|
||||
setAccountNftClient(client)
|
||||
setCreditManagerClient(client)
|
||||
}, [client, setAccountNftClient, setCreditManagerClient])
|
||||
|
||||
useEffect(() => {
|
||||
if (!userWalletAddress || !networkConfig?.contracts.creditManager) return
|
||||
setCreditManagerMsgComposer(userWalletAddress, networkConfig.contracts.creditManager)
|
||||
}, [userWalletAddress, networkConfig, setCreditManagerMsgComposer])
|
||||
|
||||
return <>{children}</>
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
@import 'src/styles/master';
|
||||
|
||||
.overlay {
|
||||
background-color: $alphaBlack60;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
|
||||
.loader {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
justify-content: center;
|
||||
@include margin(4, 0);
|
||||
}
|
||||
|
||||
.button {
|
||||
@include margin(4, 0, 0);
|
||||
}
|
||||
|
||||
.content {
|
||||
@include layoutTile;
|
||||
width: rem-calc(540);
|
||||
max-width: 100vw;
|
||||
transform: translateX(-50%);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
@include padding(4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
outline: none;
|
||||
cursor: auto;
|
||||
|
||||
.header {
|
||||
@include typoXXLcaps;
|
||||
text-align: center;
|
||||
color: $fontColorLightPrimary;
|
||||
font-weight: $fontWeightRegular;
|
||||
@include margin(0, 0, 4);
|
||||
}
|
||||
|
||||
.enableContent {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.text {
|
||||
@include typoM;
|
||||
text-align: center;
|
||||
color: $fontColorLightSecondary;
|
||||
width: 100%;
|
||||
display: block;
|
||||
|
||||
button {
|
||||
@include typoM;
|
||||
appearance: none;
|
||||
font-weight: $fontWeightSemibold;
|
||||
color: $colorPrimary;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
@include padding(2, 0);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: space(4);
|
||||
.wallet,
|
||||
button,
|
||||
a {
|
||||
background: transparent;
|
||||
@include padding(2);
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
appearance: none;
|
||||
border: none;
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
border-radius: space(2);
|
||||
cursor: pointer;
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
img,
|
||||
.info .name {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $alphaWhite10;
|
||||
}
|
||||
|
||||
img {
|
||||
height: space(15);
|
||||
width: space(15);
|
||||
}
|
||||
|
||||
.info {
|
||||
font-weight: $fontWeightRegular;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: space(5);
|
||||
|
||||
.name {
|
||||
@include typoLcaps;
|
||||
color: $fontColorLightPrimary;
|
||||
}
|
||||
|
||||
.description {
|
||||
@include margin(1, 0, 0);
|
||||
color: $fontColorLightTertiary;
|
||||
text-align: left;
|
||||
@include typoM;
|
||||
|
||||
&.capitalize {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
canvas {
|
||||
max-width: 90vw;
|
||||
max-height: 90vw;
|
||||
height: rem-calc(320);
|
||||
width: rem-calc(320);
|
||||
border: space(3) solid $colorWhite;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
import { ChainInfoID, WalletManagerProvider, WalletType } from '@marsprotocol/wallet-connector'
|
||||
import { CircularProgress, SVG } from 'components/common'
|
||||
import buttonStyles from 'components/common/Button/Button.module.scss'
|
||||
import { NETWORK_CONFIG } from 'configs/osmo-test-4'
|
||||
import { SESSION_WALLET_KEY } from 'constants/appConstants'
|
||||
import KeplrImage from 'images/keplr-wallet-extension.png'
|
||||
import WalletConnectImage from 'images/walletconnect-keplr.png'
|
||||
import { FC } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import useStore from 'store'
|
||||
|
||||
import styles from './CosmosWalletConnectProvider.module.scss'
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export const CosmosWalletConnectProvider: FC<Props> = ({ children }) => {
|
||||
const { t } = useTranslation()
|
||||
const chainId = useStore((s) => s.currentNetwork)
|
||||
|
||||
return (
|
||||
<WalletManagerProvider
|
||||
chainInfoOverrides={{
|
||||
[ChainInfoID.OsmosisTestnet]: {
|
||||
rpc: NETWORK_CONFIG.rpcUrl,
|
||||
rest: NETWORK_CONFIG.restUrl,
|
||||
},
|
||||
}}
|
||||
classNames={{
|
||||
modalContent: styles.content,
|
||||
modalOverlay: styles.overlay,
|
||||
modalHeader: styles.header,
|
||||
modalCloseButton: styles.close,
|
||||
walletList: styles.list,
|
||||
wallet: styles.wallet,
|
||||
walletImage: styles.image,
|
||||
walletInfo: styles.info,
|
||||
walletName: styles.name,
|
||||
walletDescription: styles.description,
|
||||
textContent: styles.text,
|
||||
}}
|
||||
closeIcon={<SVG.Close />}
|
||||
defaultChainId={chainId}
|
||||
enabledWalletTypes={[WalletType.Keplr, WalletType.WalletConnectKeplr]}
|
||||
enablingMeta={{
|
||||
text: <Trans i18nKey='global.walletResetText' />,
|
||||
textClassName: styles.text,
|
||||
buttonText: <Trans i18nKey='global.walletReset' />,
|
||||
buttonClassName: ` ${buttonStyles.button} ${buttonStyles.primary} ${buttonStyles.small} ${buttonStyles.solid} ${styles.button}`,
|
||||
contentClassName: styles.enableContent,
|
||||
}}
|
||||
enablingStringOverride={t('global.connectingToWallet')}
|
||||
localStorageKey={SESSION_WALLET_KEY}
|
||||
renderLoader={() => (
|
||||
<div className={styles.loader}>
|
||||
<CircularProgress size={30} />
|
||||
</div>
|
||||
)}
|
||||
walletConnectClientMeta={{
|
||||
name: 'Mars Protocol',
|
||||
description:
|
||||
'Lend, borrow and earn on the galaxy`s most powerful credit protocol or enter the Fields of Mars for advanced DeFi strategies.',
|
||||
url: 'https://marsprotocol.io',
|
||||
icons: ['https://marsprotocol.io/favicon.svg'],
|
||||
}}
|
||||
walletMetaOverride={{
|
||||
[WalletType.Keplr]: {
|
||||
description: <Trans i18nKey='global.keplrBrowserExtension' />,
|
||||
imageUrl: KeplrImage.src,
|
||||
},
|
||||
[WalletType.WalletConnectKeplr]: {
|
||||
name: <Trans i18nKey='global.walletConnect' />,
|
||||
description: <Trans i18nKey='global.walletConnectDescription' />,
|
||||
imageUrl: WalletConnectImage.src,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</WalletManagerProvider>
|
||||
)
|
||||
}
|
43
src/components/common/DisplayCurrency/DisplayCurrency.tsx
Normal file
43
src/components/common/DisplayCurrency/DisplayCurrency.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { Coin } from '@cosmjs/stargate'
|
||||
import { AnimatedNumber } from 'components/common'
|
||||
import useStore from 'store'
|
||||
|
||||
interface Props {
|
||||
coin: Coin
|
||||
prefixClass?: string
|
||||
valueClass?: string
|
||||
isApproximation?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const DisplayCurrency = ({
|
||||
coin,
|
||||
prefixClass,
|
||||
valueClass,
|
||||
isApproximation,
|
||||
className,
|
||||
}: Props) => {
|
||||
const displayCurrency = useStore((s) => s.displayCurrency)
|
||||
const convertToDisplayCurrency = useStore((s) => s.convertToDisplayCurrency)
|
||||
|
||||
const amount = convertToDisplayCurrency(coin)
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{displayCurrency.prefix && (
|
||||
<span className={prefixClass}>
|
||||
{displayCurrency.prefix}
|
||||
{isApproximation && '~'}
|
||||
</span>
|
||||
)}
|
||||
<span className={valueClass}>
|
||||
<AnimatedNumber
|
||||
amount={amount}
|
||||
minDecimals={displayCurrency.decimals}
|
||||
maxDecimals={displayCurrency.decimals}
|
||||
/>
|
||||
</span>
|
||||
{displayCurrency.suffix && <span className={valueClass}>{displayCurrency.suffix}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
109
src/components/common/Footer/Footer.module.scss
Normal file
109
src/components/common/Footer/Footer.module.scss
Normal file
@ -0,0 +1,109 @@
|
||||
@import 'src/styles/master';
|
||||
|
||||
.footer {
|
||||
@include padding(8, 0, 24);
|
||||
background-color: $backgroundFooter;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: space(-1);
|
||||
width: calc(100% - #{$spacingBase * 6});
|
||||
|
||||
.widthBox {
|
||||
width: $contentWidth;
|
||||
max-width: 100vw;
|
||||
@include padding(0, 3);
|
||||
}
|
||||
|
||||
.links {
|
||||
flex: 0 0 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.column1,
|
||||
.column2,
|
||||
.column3 {
|
||||
flex: 0 0 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include margin(0, 0, 6);
|
||||
}
|
||||
.placeholder {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-weight: $fontWeightSemibold;
|
||||
}
|
||||
|
||||
.item {
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.5s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.header,
|
||||
.item {
|
||||
text-decoration: none;
|
||||
color: $fontColorLightPrimary;
|
||||
@include margin(0, 0, 2);
|
||||
}
|
||||
|
||||
.socials {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
list-style: none;
|
||||
@include margin(2, 0, 0);
|
||||
|
||||
li {
|
||||
width: rem-calc(30);
|
||||
height: rem-calc(30);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@include margin(6, 8, 0, 0);
|
||||
|
||||
&:last-child {
|
||||
@include margin(6, 0, 0);
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.5s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $bpMediumLow) {
|
||||
.footer {
|
||||
@include padding(8, 0);
|
||||
left: space(-4);
|
||||
width: calc(100% + (8 * #{$spacingBase}px));
|
||||
|
||||
.links {
|
||||
flex-wrap: nowrap;
|
||||
|
||||
.column1,
|
||||
.column2,
|
||||
.column3 {
|
||||
flex: 0 0 33%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
203
src/components/common/Footer/Footer.tsx
Normal file
203
src/components/common/Footer/Footer.tsx
Normal file
@ -0,0 +1,203 @@
|
||||
import { SVG } from 'components/common'
|
||||
import { FIELDS_FEATURE } from 'constants/appConstants'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useStore from 'store'
|
||||
import { DocURL } from 'types/enums/docURL'
|
||||
|
||||
import styles from './Footer.module.scss'
|
||||
|
||||
export const Footer = () => {
|
||||
const { t } = useTranslation()
|
||||
const networkConfig = useStore((s) => s.networkConfig)
|
||||
|
||||
return (
|
||||
<footer className={styles.footer}>
|
||||
<div className={styles.widthBox}>
|
||||
<div className={styles.links}>
|
||||
<div className={styles.column1}>
|
||||
<div className={styles.header}>{t('global.mars')}</div>
|
||||
<a
|
||||
className={styles.item}
|
||||
href='https://osmosis.marsprotocol.io/#/redbank'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
title={t('global.redBank')}
|
||||
>
|
||||
{t('global.redBank')}
|
||||
</a>
|
||||
{FIELDS_FEATURE && (
|
||||
<a
|
||||
className={styles.item}
|
||||
href='https://osmosis.marsprotocol.io/#/farm'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
title={t('global.fields')}
|
||||
>
|
||||
{t('global.fields')}
|
||||
</a>
|
||||
)}
|
||||
<a
|
||||
className={styles.item}
|
||||
href={networkConfig?.councilUrl}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
title={t('global.council')}
|
||||
>
|
||||
{t('global.council')}
|
||||
</a>
|
||||
<a
|
||||
className={styles.item}
|
||||
href='http://explorer.marsprotocol.io/'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
title={t('global.blockExplorer')}
|
||||
>
|
||||
{t('global.blockExplorer')}
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.column2}>
|
||||
<div className={styles.header}>{t('global.documentation')}</div>
|
||||
<a
|
||||
className={styles.item}
|
||||
href='https://docs.marsprotocol.io'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
title={t('global.docs')}
|
||||
>
|
||||
{t('global.docs')}
|
||||
</a>
|
||||
<a
|
||||
className={styles.item}
|
||||
href='https://whitepaper.marsprotocol.io'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
title={t('global.whitepaper')}
|
||||
>
|
||||
{t('global.whitepaper')}
|
||||
</a>
|
||||
<a
|
||||
className={styles.item}
|
||||
href={DocURL.TERMS_OF_SERVICE_URL}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
title={t('global.termsOfService')}
|
||||
>
|
||||
{t('global.termsOfService')}
|
||||
</a>
|
||||
<a
|
||||
className={styles.item}
|
||||
href={DocURL.COOKIE_POLICY_URL}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
title={t('global.cookiePolicy')}
|
||||
>
|
||||
{t('global.cookiePolicy')}
|
||||
</a>
|
||||
<a
|
||||
className={styles.item}
|
||||
href={DocURL.PRIVACY_POLICY_URL}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
title={t('global.privacyPolicy')}
|
||||
>
|
||||
{t('global.privacyPolicy')}
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.column3}>
|
||||
<div className={styles.header}>{t('global.community')}</div>
|
||||
<a
|
||||
className={styles.item}
|
||||
href='https://blog.marsprotocol.io'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
title={t('global.blog')}
|
||||
>
|
||||
{t('global.blog')}
|
||||
</a>
|
||||
<a
|
||||
className={styles.item}
|
||||
href='https://forum.marsprotocol.io'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
title={t('global.forum')}
|
||||
>
|
||||
{t('global.forum')}
|
||||
</a>
|
||||
<ul className={styles.socials}>
|
||||
<li>
|
||||
<a
|
||||
href='https://twitter.marsprotocol.io'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
title='Twitter'
|
||||
>
|
||||
<SVG.Twitter />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href='https://medium.marsprotocol.io'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
title='<Medium'
|
||||
>
|
||||
<SVG.Medium />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href='https://discord.marsprotocol.io'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
title='Discord'
|
||||
>
|
||||
<SVG.Discord />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href='https://reddit.marsprotocol.io'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
title='Reddit'
|
||||
>
|
||||
<SVG.Reddit />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href='https://telegram.marsprotocol.io'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
title='Telegram'
|
||||
>
|
||||
<SVG.Telegram />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href='https://youtube.marsprotocol.io'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
title='YoutTube'
|
||||
>
|
||||
<SVG.YouTube />
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href='https://github.marsprotocol.io'
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
title='Github'
|
||||
>
|
||||
<SVG.Github />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
10
src/components/common/Header/Connect.tsx
Normal file
10
src/components/common/Header/Connect.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { useWallet, WalletConnectionStatus } from '@marsprotocol/wallet-connector'
|
||||
import { ConnectButton, ConnectedButton } from 'components/common'
|
||||
|
||||
export const Connect = () => {
|
||||
const { status } = useWallet()
|
||||
|
||||
if (status === WalletConnectionStatus.Connected) return <ConnectedButton />
|
||||
|
||||
return <ConnectButton />
|
||||
}
|
29
src/components/common/Header/ConnectButton.module.scss
Normal file
29
src/components/common/Header/ConnectButton.module.scss
Normal file
@ -0,0 +1,29 @@
|
||||
@import 'src/styles/master';
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
|
||||
&.outline {
|
||||
border: 1px solid $fontColorLightPrimary;
|
||||
color: $fontColorLightPrimary;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $alphaWhite40;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $alphaWhite60;
|
||||
}
|
||||
}
|
||||
|
||||
.svg {
|
||||
@include margin(0);
|
||||
height: rem-calc(16) !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
}
|
31
src/components/common/Header/ConnectButton.tsx
Normal file
31
src/components/common/Header/ConnectButton.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { useWalletManager } from '@marsprotocol/wallet-connector'
|
||||
import classNames from 'classnames'
|
||||
import { Button, SVG } from 'components/common'
|
||||
import { ReactNode } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import styles from './ConnectButton.module.scss'
|
||||
|
||||
interface Props {
|
||||
textOverride?: string | ReactNode
|
||||
disabled?: boolean
|
||||
color?: 'primary' | 'secondary'
|
||||
}
|
||||
|
||||
export const ConnectButton = ({ textOverride, disabled = false, color }: Props) => {
|
||||
const { connect } = useWalletManager()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Button
|
||||
color={color ? color : 'quaternary'}
|
||||
disabled={disabled}
|
||||
onClick={connect}
|
||||
className={classNames(styles.button, !color && styles.outline)}
|
||||
prefix={<SVG.Wallet className={styles.svg} />}
|
||||
text={<span className='overline'>{textOverride || t('common.connectWallet')}</span>}
|
||||
></Button>
|
||||
</div>
|
||||
)
|
||||
}
|
247
src/components/common/Header/ConnectedButton.module.scss
Normal file
247
src/components/common/Header/ConnectedButton.module.scss
Normal file
@ -0,0 +1,247 @@
|
||||
@import 'src/styles/master';
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
> span {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.svg {
|
||||
margin-top: rem-calc(-1);
|
||||
height: rem-calc(14);
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.address {
|
||||
display: none;
|
||||
font-weight: $fontWeightRegular;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clickAway {
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 99;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
top: rem-calc(38);
|
||||
@include padding(6, 6, 5.5);
|
||||
@include layoutPopover;
|
||||
width: rem-calc(420);
|
||||
max-width: calc(100vw - 24px);
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.detailsHeader {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
@include margin(0, 0, 4);
|
||||
}
|
||||
|
||||
.detailsDenom {
|
||||
@include margin(0, 2, 0, 0);
|
||||
@include typoMcaps;
|
||||
display: flex;
|
||||
height: rem-calc(31);
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.detailsBalance {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
justify-content: space-between;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.detailsBalanceAmount {
|
||||
display: flex;
|
||||
flex: 0;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
min-width: rem-calc(120);
|
||||
|
||||
> span {
|
||||
@include typoH4;
|
||||
|
||||
+ div {
|
||||
@include margin(-1, 0, 0);
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detailsButton {
|
||||
display: flex;
|
||||
height: rem-calc(32);
|
||||
justify-content: center;
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
@include margin(2, 0, 0);
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.detailsBody {
|
||||
flex: 0;
|
||||
width: 100%;
|
||||
|
||||
.address,
|
||||
.addressMobile,
|
||||
.addressLabel {
|
||||
color: $colorSecondaryDark;
|
||||
opacity: 1;
|
||||
@include margin(0, 0, 1);
|
||||
@include typoS;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.address,
|
||||
.addressMobile {
|
||||
font-weight: $fontWeightSemibold;
|
||||
}
|
||||
|
||||
.addressLabel {
|
||||
@include typoScaps;
|
||||
font-weight: $fontWeightRegular;
|
||||
}
|
||||
|
||||
.address {
|
||||
display: none;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: rem-calc(16);
|
||||
width: auto;
|
||||
@include margin(0, 2, 0, 0);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
flex-wrap: wrap;
|
||||
@include padding(1, 0, 0);
|
||||
> button {
|
||||
font-weight: $fontWeightRegular;
|
||||
@include typoM;
|
||||
|
||||
&:first-child {
|
||||
width: rem-calc(160);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
align-items: center;
|
||||
color: $colorSecondaryDark;
|
||||
background: transparent;
|
||||
border: none;
|
||||
@include padding(2, 0);
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.network {
|
||||
background-color: $colorSecondaryHighlight;
|
||||
text-transform: uppercase;
|
||||
border-radius: $borderRadiusL;
|
||||
@include padding(0, 2);
|
||||
@include margin(0);
|
||||
@include typoNetwork;
|
||||
position: absolute;
|
||||
top: space(-4);
|
||||
right: space(-3);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $bpMediumLow) {
|
||||
.details {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.detailsBalance {
|
||||
flex: 1;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.detailsButton {
|
||||
flex: 0 0 rem-calc(116);
|
||||
width: rem-calc(116);
|
||||
justify-content: flex-end;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.detailsHeader {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.detailsBody {
|
||||
.addressMobile {
|
||||
display: none;
|
||||
}
|
||||
.address {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $bpLargeHigh) {
|
||||
.wrapper {
|
||||
.button {
|
||||
.address {
|
||||
display: block;
|
||||
font-weight: $fontWeightRegular;
|
||||
}
|
||||
|
||||
.balance {
|
||||
margin-inline-start: space(2);
|
||||
position: relative;
|
||||
@include padding(0, 0, 0, 2);
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: space(0.5);
|
||||
bottom: space(1.5);
|
||||
height: calc(100% - #{$spacingBase}px);
|
||||
left: 0;
|
||||
border-left: 1px solid $fontColorLightPrimary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
146
src/components/common/Header/ConnectedButton.tsx
Normal file
146
src/components/common/Header/ConnectedButton.tsx
Normal file
@ -0,0 +1,146 @@
|
||||
import { ChainInfoID, SimpleChainInfoList, useWalletManager } from '@marsprotocol/wallet-connector'
|
||||
import { AnimatedNumber, Button, CircularProgress, DisplayCurrency, SVG } from 'components/common'
|
||||
import { findByDenom } from 'functions'
|
||||
import { useUserBalance } from 'hooks/queries'
|
||||
import { formatValue, lookup } from 'libs/parse'
|
||||
import { truncate } from 'libs/text'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useClipboard from 'react-use-clipboard'
|
||||
import useStore from 'store'
|
||||
import colors from 'styles/_assets.module.scss'
|
||||
|
||||
import styles from './ConnectedButton.module.scss'
|
||||
|
||||
export const ConnectedButton = () => {
|
||||
// ---------------
|
||||
// EXTERNAL HOOKS
|
||||
// ---------------
|
||||
const { disconnect } = useWalletManager()
|
||||
const { t } = useTranslation()
|
||||
|
||||
// ---------------
|
||||
// STORE
|
||||
// ---------------
|
||||
const baseCurrency = useStore((s) => s.baseCurrency)
|
||||
const chainInfo = useStore((s) => s.chainInfo)
|
||||
const userWalletAddress = useStore((s) => s.userWalletAddress)
|
||||
const userWalletName = useStore((s) => s.userWalletName)
|
||||
|
||||
// ---------------
|
||||
// LOCAL STATE
|
||||
// ---------------
|
||||
const [isCopied, setCopied] = useClipboard(userWalletAddress, {
|
||||
successDuration: 1000 * 5,
|
||||
})
|
||||
const { data, isLoading } = useUserBalance()
|
||||
|
||||
// ---------------
|
||||
// VARIABLES
|
||||
// ---------------
|
||||
const baseCurrencyBalance = Number(findByDenom(data || [], baseCurrency.denom || '')?.amount || 0)
|
||||
const explorerName =
|
||||
chainInfo && SimpleChainInfoList[chainInfo.chainId as ChainInfoID].explorerName
|
||||
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
|
||||
const viewOnFinder = useCallback(() => {
|
||||
const explorerUrl = chainInfo && SimpleChainInfoList[chainInfo.chainId as ChainInfoID].explorer
|
||||
|
||||
window.open(`${explorerUrl}account/${userWalletAddress}`, '_blank')
|
||||
}, [chainInfo, userWalletAddress])
|
||||
|
||||
const onClickAway = useCallback(() => {
|
||||
setShowDetails(false)
|
||||
}, [])
|
||||
|
||||
const currentBalanceAmount = lookup(
|
||||
baseCurrencyBalance,
|
||||
baseCurrency.denom,
|
||||
baseCurrency.decimals,
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
{chainInfo?.chainId !== ChainInfoID.Osmosis1 && (
|
||||
<span className={styles.network}>{chainInfo?.chainId}</span>
|
||||
)}
|
||||
<Button
|
||||
className={styles.button}
|
||||
onClick={() => {
|
||||
setShowDetails(!showDetails)
|
||||
}}
|
||||
color='tertiary'
|
||||
prefix={<SVG.Osmo className={styles.svg} />}
|
||||
text={
|
||||
<>
|
||||
<span className={styles.address}>
|
||||
{userWalletName ? userWalletName : truncate(userWalletAddress, [2, 4])}
|
||||
</span>
|
||||
<span className={`${styles.balance} number`}>
|
||||
{!isLoading ? (
|
||||
`${formatValue(
|
||||
currentBalanceAmount,
|
||||
2,
|
||||
2,
|
||||
true,
|
||||
false,
|
||||
` ${chainInfo?.stakeCurrency?.coinDenom}`,
|
||||
)}`
|
||||
) : (
|
||||
<CircularProgress className={styles.circularProgress} size={12} />
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
{showDetails && (
|
||||
<>
|
||||
<div className={styles.details}>
|
||||
<div className={styles.detailsHeader}>
|
||||
<div className={styles.detailsBalance}>
|
||||
<div className={styles.detailsDenom}>{chainInfo?.stakeCurrency?.coinDenom}</div>
|
||||
<div className={`${styles.detailsBalanceAmount}`}>
|
||||
<AnimatedNumber amount={currentBalanceAmount} abbreviated={false} />
|
||||
<DisplayCurrency
|
||||
className='s faded'
|
||||
coin={{
|
||||
amount: baseCurrencyBalance.toString(),
|
||||
denom: baseCurrency.denom,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.detailsButton}>
|
||||
<Button color='secondary' onClick={disconnect} text={t('common.disconnect')} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.detailsBody}>
|
||||
<p className={styles.addressLabel}>
|
||||
{userWalletName ? `‘${userWalletName}’` : t('common.yourAddress')}
|
||||
</p>
|
||||
<p className={styles.address}>{userWalletAddress}</p>
|
||||
<p className={styles.addressMobile}>{truncate(userWalletAddress, [14, 14])}</p>
|
||||
<div className={styles.buttons}>
|
||||
<button className={styles.copy} onClick={setCopied}>
|
||||
<SVG.Copy color={colors.secondaryDark} />
|
||||
{isCopied ? (
|
||||
<>
|
||||
{t('common.copied')} <SVG.Check color={colors.secondaryDark} />
|
||||
</>
|
||||
) : (
|
||||
<>{t('common.copy')}</>
|
||||
)}
|
||||
</button>
|
||||
<button className={styles.external} onClick={viewOnFinder}>
|
||||
<SVG.ExternalLink /> {t('common.viewOnExplorer', { explorer: explorerName })}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.clickAway} onClick={onClickAway} role='button' />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
86
src/components/common/Header/Header.module.scss
Normal file
86
src/components/common/Header/Header.module.scss
Normal file
@ -0,0 +1,86 @@
|
||||
@import 'src/styles/master';
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include margin(4, 0, 8);
|
||||
|
||||
.connector {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $bpMediumLow) {
|
||||
.header {
|
||||
max-width: $contentWidth;
|
||||
margin: space(4) auto space(6);
|
||||
|
||||
.connector {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: block;
|
||||
@include typoNav;
|
||||
color: $fontColorLightPrimary;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.5s;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
color: $fontColorLightPrimary;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
@include padding(0, 8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
gap: space(5);
|
||||
}
|
||||
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
@include layoutLogo;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.active {
|
||||
opacity: 1;
|
||||
pointer-events: none;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: calc(100% - 5px);
|
||||
bottom: 0;
|
||||
height: 1px;
|
||||
background-color: $fontColorLightPrimary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $bpLargeLow) {
|
||||
.header {
|
||||
.navbar {
|
||||
gap: space(8);
|
||||
}
|
||||
}
|
||||
}
|
50
src/components/common/Header/Header.tsx
Normal file
50
src/components/common/Header/Header.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import classNames from 'classnames'
|
||||
import { IncentivesButton, Settings, SVG } from 'components/common'
|
||||
import { FIELDS_FEATURE } from 'constants/appConstants'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useStore from 'store'
|
||||
|
||||
import { Connect } from './Connect'
|
||||
import styles from './Header.module.scss'
|
||||
|
||||
export const Header = () => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const networkConfig = useStore((s) => s.networkConfig)
|
||||
|
||||
return (
|
||||
<header className={styles.header}>
|
||||
<div className={styles.logo}>
|
||||
<SVG.Logo />
|
||||
</div>
|
||||
<div className={styles.navbar}>
|
||||
<Link href='/redbank' passHref>
|
||||
<a className={classNames(styles.nav, !router.pathname.includes('farm') && styles.active)}>
|
||||
{t('global.redBank')}
|
||||
</a>
|
||||
</Link>
|
||||
<Link href='/farm' passHref>
|
||||
<a
|
||||
className={classNames(
|
||||
!FIELDS_FEATURE && styles.disabled,
|
||||
styles.nav,
|
||||
router.pathname.includes('farm') && styles.active,
|
||||
)}
|
||||
>
|
||||
{t('global.fields')}
|
||||
</a>
|
||||
</Link>
|
||||
<a className={styles.nav} href={networkConfig?.councilUrl} target='_blank' rel='noreferrer'>
|
||||
{t('global.council')}
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.connector}>
|
||||
<IncentivesButton />
|
||||
<Connect />
|
||||
<Settings />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
278
src/components/common/Header/IncentivesButton.module.scss
Normal file
278
src/components/common/Header/IncentivesButton.module.scss
Normal file
@ -0,0 +1,278 @@
|
||||
@import 'src/styles/master';
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
flex-wrap: nowrap;
|
||||
border-radius: $borderRadiusXXL;
|
||||
height: rem-calc(34);
|
||||
@include padding(0.5, 3, 0);
|
||||
@include margin(0, 2, 0, 0);
|
||||
@include typoS;
|
||||
cursor: pointer;
|
||||
color: $colorWhite;
|
||||
border: 1px solid $alphaWhite60;
|
||||
outline: none;
|
||||
background: $alphaBlack10;
|
||||
|
||||
svg {
|
||||
margin-top: space(-0.5);
|
||||
height: rem-calc(19);
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.balance {
|
||||
font-weight: $fontWeightRegular;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding-inline-start: rem-calc(8);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: 1px solid $colorWhite;
|
||||
background-color: $alphaWhite10;
|
||||
}
|
||||
}
|
||||
|
||||
.buttonHighlight {
|
||||
@include layoutIncentiveButton;
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
max-width: calc(100vw - 2 * #{$spacingBase}px);
|
||||
top: rem-calc(56);
|
||||
width: rem-calc(390);
|
||||
right: space(1);
|
||||
z-index: 100;
|
||||
@include layoutPopover;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
top: rem-calc(12);
|
||||
right: rem-calc(16);
|
||||
|
||||
svg {
|
||||
color: $colorSecondaryDark;
|
||||
}
|
||||
}
|
||||
|
||||
.detailsHeader {
|
||||
display: flex;
|
||||
flex: 0;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
@include padding(4, 0);
|
||||
@include margin(0);
|
||||
position: relative;
|
||||
border-bottom: 1px solid $alphaBlack20;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.detailsHead {
|
||||
@include margin(0);
|
||||
@include typoScaps;
|
||||
color: $colorSecondaryDark;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.detailsBody {
|
||||
flex: 0;
|
||||
width: 100%;
|
||||
@include padding(4);
|
||||
color: $colorSecondaryDark;
|
||||
|
||||
.successContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.successTitle {
|
||||
text-align: center;
|
||||
color: $colorSecondaryDark;
|
||||
@include margin(0, 0, 4);
|
||||
}
|
||||
|
||||
.succcessTxHash {
|
||||
display: flex;
|
||||
@include margin(0, 0, 4);
|
||||
|
||||
.label {
|
||||
@include margin(0, 2, 0, 0);
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container,
|
||||
.total {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
@include padding(4, 0, 0);
|
||||
border-bottom: 1px solid $colorSecondaryDark;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&.info {
|
||||
justify-content: center;
|
||||
@include padding(4, 0);
|
||||
|
||||
p {
|
||||
@include margin(0, 0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
.position {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
flex-wrap: nowrap;
|
||||
@include padding(0, 0, 4);
|
||||
|
||||
.head {
|
||||
@include typoScaps;
|
||||
}
|
||||
|
||||
p {
|
||||
width: 100%;
|
||||
@include margin(0);
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 1;
|
||||
min-height: rem-calc(12);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.subhead {
|
||||
color: $colorSecondaryDark;
|
||||
opacity: 0.6;
|
||||
@include margin(1, 0);
|
||||
@include typoS;
|
||||
}
|
||||
|
||||
.token {
|
||||
@include typoM;
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
min-height: rem-calc(12);
|
||||
flex: 0 0 rem-calc(76);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@include margin(0, 0, 0, 3);
|
||||
|
||||
p {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.headline {
|
||||
@include typoXXScaps;
|
||||
font-weight: $fontWeightSemibold;
|
||||
}
|
||||
|
||||
.tokenAmount {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
@include typoM;
|
||||
}
|
||||
|
||||
.tokenValue {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
@include margin(1, 0);
|
||||
@include typoS;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.total {
|
||||
border-bottom: none;
|
||||
|
||||
.position {
|
||||
.label {
|
||||
.subhead {
|
||||
opacity: 1;
|
||||
font-weight: $fontWeightSemibold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.claimButton {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex: 0 0 100%;
|
||||
margin: 0 auto;
|
||||
@include padding(6, 0, 0);
|
||||
flex-wrap: wrap;
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error {
|
||||
@include margin(2, 0, 0);
|
||||
@include typoS;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-weight: $fontWeightSemibold;
|
||||
color: $colorInfoLoss;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clickAway {
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 99;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $bpMediumLow) {
|
||||
.wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.details {
|
||||
right: unset;
|
||||
top: rem-calc(38);
|
||||
left: rem-calc(-155);
|
||||
}
|
||||
|
||||
.detailsBody {
|
||||
.claimButton {
|
||||
button {
|
||||
width: rem-calc(160);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveGradient {
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
209
src/components/common/Header/IncentivesButton.tsx
Normal file
209
src/components/common/Header/IncentivesButton.tsx
Normal file
@ -0,0 +1,209 @@
|
||||
import { ExecuteResult } from '@cosmjs/cosmwasm-stargate'
|
||||
import { ChainInfoID, SimpleChainInfoList } from '@marsprotocol/wallet-connector'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import classNames from 'classnames'
|
||||
import { AnimatedNumber, Button, DisplayCurrency, SVG, Tooltip, TxLink } from 'components/common'
|
||||
import { MARS_DECIMALS, MARS_SYMBOL } from 'constants/appConstants'
|
||||
import { getClaimUserRewardsMsgOptions } from 'functions/messages'
|
||||
import { useEstimateFee } from 'hooks/queries'
|
||||
import { lookup, lookupDenomBySymbol, lookupSymbol } from 'libs/parse'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useStore from 'store'
|
||||
import { QUERY_KEYS } from 'types/enums/queryKeys'
|
||||
|
||||
import styles from './IncentivesButton.module.scss'
|
||||
|
||||
export const IncentivesButton = () => {
|
||||
// ---------------
|
||||
// EXTERNAL HOOKS
|
||||
// ---------------
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// ---------------
|
||||
// STORE STATE
|
||||
// ---------------
|
||||
const client = useStore((s) => s.client)
|
||||
const otherAssets = useStore((s) => s.otherAssets)
|
||||
const userWalletAddress = useStore((s) => s.userWalletAddress)
|
||||
const unclaimedRewards = useStore((s) => s.userUnclaimedRewards)
|
||||
const incentivesContractAddress = useStore((s) => s.networkConfig?.contracts.incentives)
|
||||
const chainInfo = useStore((s) => s.chainInfo)
|
||||
const executeMsg = useStore((s) => s.executeMsg)
|
||||
|
||||
// ---------------
|
||||
// LOCAL STATE
|
||||
// ---------------
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
const [disabled, setDisabled] = useState(true)
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
const [response, setResponse] = useState<ExecuteResult>()
|
||||
const [error, setError] = useState<string>()
|
||||
const [hasUnclaimedRewards, setHasUnclaimedRewards] = useState(false)
|
||||
|
||||
// ---------------
|
||||
// LOCAL VARIABLES
|
||||
// ---------------
|
||||
const marsDenom = lookupDenomBySymbol(MARS_SYMBOL, otherAssets)
|
||||
const explorerUrl = chainInfo && SimpleChainInfoList[chainInfo.chainId as ChainInfoID].explorer
|
||||
|
||||
// ---------------
|
||||
// FUNCTIONS
|
||||
// ---------------
|
||||
const onClickAway = useCallback(() => {
|
||||
setShowDetails(false)
|
||||
setResponse(undefined)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setHasUnclaimedRewards(Number(unclaimedRewards) > 0)
|
||||
}, [unclaimedRewards])
|
||||
|
||||
const txMsgOptions = useMemo(() => {
|
||||
if (!hasUnclaimedRewards) return
|
||||
return getClaimUserRewardsMsgOptions()
|
||||
}, [hasUnclaimedRewards])
|
||||
|
||||
const { data: fee } = useEstimateFee({
|
||||
msg: txMsgOptions?.msg,
|
||||
funds: [],
|
||||
contract: incentivesContractAddress,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
setDisabled(!hasUnclaimedRewards)
|
||||
setSubmitted(false)
|
||||
return
|
||||
}
|
||||
if (response?.transactionHash) {
|
||||
setDisabled(true)
|
||||
setSubmitted(false)
|
||||
return
|
||||
}
|
||||
|
||||
setDisabled(!hasUnclaimedRewards)
|
||||
}, [error, response, hasUnclaimedRewards])
|
||||
|
||||
const claimRewards = async () => {
|
||||
if (!incentivesContractAddress || !client) {
|
||||
setError(t('error.errorClaim'))
|
||||
setDisabled(true)
|
||||
setSubmitted(false)
|
||||
return
|
||||
}
|
||||
setDisabled(true)
|
||||
setSubmitted(true)
|
||||
setError(undefined)
|
||||
|
||||
if (!fee || !txMsgOptions) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await executeMsg({
|
||||
msg: txMsgOptions.msg,
|
||||
funds: [],
|
||||
contract: incentivesContractAddress,
|
||||
fee,
|
||||
})
|
||||
|
||||
setResponse(res)
|
||||
queryClient.invalidateQueries([QUERY_KEYS.REDBANK])
|
||||
} catch (error) {
|
||||
const e = error as { message: string }
|
||||
setError(e.message as string)
|
||||
}
|
||||
}
|
||||
|
||||
if (!userWalletAddress) return null
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<button
|
||||
className={classNames(
|
||||
Number(unclaimedRewards) > 1000000
|
||||
? `${styles.button} ${styles.buttonHighlight}`
|
||||
: styles.button,
|
||||
)}
|
||||
onClick={() => {
|
||||
setShowDetails(!showDetails)
|
||||
}}
|
||||
>
|
||||
<SVG.Logo />
|
||||
<DisplayCurrency
|
||||
className={styles.balance}
|
||||
coin={{
|
||||
amount: unclaimedRewards,
|
||||
denom: marsDenom,
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{showDetails && (
|
||||
<>
|
||||
<div className={styles.details}>
|
||||
<div className={styles.detailsHeader}>
|
||||
<p className={styles.detailsHead}>{t('incentives.marsRewardsCenter')}</p>
|
||||
<div className={styles.tooltip}>
|
||||
<Tooltip content={t('incentives.marsRewardsCenterTooltip')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.detailsBody}>
|
||||
{response ? (
|
||||
<div className={`${styles.container} ${styles.info}`}>
|
||||
<p className='m'>{t('incentives.successfullyClaimed')}</p>
|
||||
<TxLink
|
||||
hash={response?.transactionHash || ''}
|
||||
link={`${explorerUrl}txs/${response?.transactionHash}`}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.position}>
|
||||
<div className={styles.label}>
|
||||
<p className={styles.token}>{lookupSymbol(marsDenom, otherAssets)}</p>
|
||||
<p className={styles.subhead}>{t('redbank.redBankRewards')}</p>
|
||||
</div>
|
||||
<div className={styles.value}>
|
||||
<AnimatedNumber
|
||||
className={styles.tokenAmount}
|
||||
amount={lookup(Number(unclaimedRewards) || 0, MARS_SYMBOL, MARS_DECIMALS)}
|
||||
maxDecimals={MARS_DECIMALS}
|
||||
minDecimals={2}
|
||||
/>
|
||||
<DisplayCurrency
|
||||
className={styles.tokenValue}
|
||||
coin={{
|
||||
amount: unclaimedRewards,
|
||||
denom: marsDenom,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.claimButton}>
|
||||
<Button
|
||||
disabled={disabled || submitted || (!fee && !response && hasUnclaimedRewards)}
|
||||
showProgressIndicator={(!fee && !response && hasUnclaimedRewards) || submitted}
|
||||
text={
|
||||
Number(unclaimedRewards) > 0 && !disabled
|
||||
? t('incentives.claimRewards')
|
||||
: t('incentives.nothingToClaim')
|
||||
}
|
||||
onClick={() => (submitted ? null : claimRewards())}
|
||||
color='primary'
|
||||
/>
|
||||
{error && <div className={styles.error}>{error}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.clickAway} onClick={onClickAway} role='button' />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
120
src/components/common/Header/Settings.module.scss
Normal file
120
src/components/common/Header/Settings.module.scss
Normal file
@ -0,0 +1,120 @@
|
||||
@import 'src/styles/master';
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
display: inline;
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
max-width: calc(100vw - 2 * #{$spacingBase}px);
|
||||
top: rem-calc(40);
|
||||
width: rem-calc(240);
|
||||
right: space(1);
|
||||
z-index: 100;
|
||||
@include layoutPopover;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
@include padding(4, 0, 2);
|
||||
@include margin(0);
|
||||
position: relative;
|
||||
border-bottom: 1px solid $alphaBlack20;
|
||||
text-align: center;
|
||||
|
||||
.text {
|
||||
@include margin(0);
|
||||
@include typoScaps;
|
||||
color: $colorSecondaryDark;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.settings {
|
||||
@include padding(2, 4, 4);
|
||||
|
||||
.setting {
|
||||
color: $colorSecondaryDark;
|
||||
flex: 0 0 100%;
|
||||
flex-wrap: wrap;
|
||||
display: flex;
|
||||
|
||||
.name {
|
||||
@include margin(1, 0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include typoS;
|
||||
position: relative;
|
||||
@include padding(0, 5, 0, 0);
|
||||
|
||||
.tooltip {
|
||||
color: $alphaBlack90;
|
||||
right: 0;
|
||||
top: space(-1);
|
||||
margin-left: space(2);
|
||||
width: rem-calc(14);
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
@include margin(1, 0);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
justify-content: flex-end;
|
||||
gap: space(2);
|
||||
|
||||
.button {
|
||||
appearance: none;
|
||||
background: none;
|
||||
border-radius: $borderRadiusXL;
|
||||
border: 1px solid $colorPrimary;
|
||||
margin-left: 0;
|
||||
@include padding(1, 4);
|
||||
transition: all 200ms ease;
|
||||
color: $colorPrimary;
|
||||
|
||||
&.solid {
|
||||
background: $colorPrimary;
|
||||
color: $colorWhite;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
@include margin(0, 0, 0, 2);
|
||||
}
|
||||
|
||||
.customSlippageBtn {
|
||||
height: rem-calc(16);
|
||||
width: rem-calc(20);
|
||||
color: unset;
|
||||
}
|
||||
|
||||
.clickAway {
|
||||
display: block;
|
||||
position: fixed;
|
||||
z-index: 99;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
145
src/components/common/Header/Settings.tsx
Normal file
145
src/components/common/Header/Settings.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
import { useWallet, WalletConnectionStatus } from '@marsprotocol/wallet-connector'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import classNames from 'classnames/bind'
|
||||
import { Button, NumberInput, SVG, Toggle, Tooltip } from 'components/common'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useStore from 'store'
|
||||
|
||||
import styles from './Settings.module.scss'
|
||||
|
||||
export const Settings = () => {
|
||||
const { t } = useTranslation()
|
||||
const inputPlaceholder = '...'
|
||||
|
||||
const slippages = [0.02, 0.03]
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
const slippage = useStore((s) => s.slippage)
|
||||
const [customSlippage, setCustomSlippage] = useState<string>(inputPlaceholder)
|
||||
const [inputRef, setInputRef] = useState<React.RefObject<HTMLInputElement>>()
|
||||
const [isCustom, setIsCustom] = useState(false)
|
||||
const enableAnimations = useStore((s) => s.enableAnimations)
|
||||
const { status } = useWallet()
|
||||
|
||||
const onInputChange = (value: number) => {
|
||||
setCustomSlippage(value.toString())
|
||||
if (value.toString() === '') {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const onInputBlur = () => {
|
||||
setIsCustom(false)
|
||||
|
||||
if (!customSlippage) {
|
||||
setCustomSlippage(inputPlaceholder)
|
||||
useStore.setState({ slippage })
|
||||
return
|
||||
}
|
||||
|
||||
const value = Number(customSlippage || 0) / 100
|
||||
if (slippages.includes(value)) {
|
||||
setCustomSlippage(inputPlaceholder)
|
||||
useStore.setState({ slippage: value })
|
||||
return
|
||||
}
|
||||
|
||||
useStore.setState({ slippage: new BigNumber(customSlippage).div(100).toNumber() })
|
||||
}
|
||||
|
||||
const onInputFocus = () => {
|
||||
setIsCustom(true)
|
||||
}
|
||||
|
||||
const changeReduceMotion = (reduce: boolean) => {
|
||||
useStore.setState({ enableAnimations: !reduce })
|
||||
localStorage.setItem('enableAnimations', reduce ? 'false' : 'true')
|
||||
}
|
||||
|
||||
if (status !== WalletConnectionStatus.Connected) return null
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
variant='round'
|
||||
color='tertiary'
|
||||
suffix={<SVG.Settings />}
|
||||
onClick={() => setShowDetails(true)}
|
||||
/>
|
||||
{showDetails && (
|
||||
<>
|
||||
<div className={styles.details}>
|
||||
<div className={styles.header}>
|
||||
<p className={styles.text}>{t('common.settings')}</p>
|
||||
</div>
|
||||
<div className={styles.settings}>
|
||||
<div className={styles.setting}>
|
||||
<div className={styles.name}>
|
||||
{t('common.reduceMotion')}
|
||||
<Tooltip content={t('common.tooltips.reduceMotion')} className={styles.tooltip} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<Toggle
|
||||
name='reduceMotionToggle'
|
||||
checked={!enableAnimations}
|
||||
onChange={changeReduceMotion}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.header}>
|
||||
<p className={styles.text}>{t('fields.settings')}</p>
|
||||
</div>
|
||||
<div className={styles.settings}>
|
||||
<div className={styles.setting}>
|
||||
<div className={styles.name}>
|
||||
{t('common.slippage')}
|
||||
<Tooltip content={t('fields.tooltips.slippage')} className={styles.tooltip} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
{slippages.map((value) => (
|
||||
<button
|
||||
key={`slippage-${value}`}
|
||||
onClick={() => {
|
||||
useStore.setState({ slippage: value })
|
||||
}}
|
||||
className={classNames([
|
||||
styles.button,
|
||||
slippage === value && !isCustom ? styles.solid : '',
|
||||
])}
|
||||
>
|
||||
{value * 100}%
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
onClick={() => inputRef?.current?.focus()}
|
||||
className={classNames([
|
||||
styles.button,
|
||||
!slippages.includes(slippage) || isCustom ? styles.solid : '',
|
||||
])}
|
||||
>
|
||||
<NumberInput
|
||||
onRef={setInputRef}
|
||||
onChange={onInputChange}
|
||||
onBlur={onInputBlur}
|
||||
onFocus={onInputFocus}
|
||||
value={customSlippage}
|
||||
maxValue={10}
|
||||
maxDecimals={1}
|
||||
maxLength={3}
|
||||
className={styles.customSlippageBtn}
|
||||
/>
|
||||
%
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.clickAway} onClick={() => setShowDetails(false)} role='button' />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
13
src/components/common/Highlight/Highlight.module.scss
Normal file
13
src/components/common/Highlight/Highlight.module.scss
Normal file
@ -0,0 +1,13 @@
|
||||
.highlight {
|
||||
transition: all 200ms ease-in-out 100ms;
|
||||
position: relative;
|
||||
|
||||
&.show {
|
||||
z-index: 21;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.hide {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
19
src/components/common/Highlight/Highlight.tsx
Normal file
19
src/components/common/Highlight/Highlight.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import classNames from 'classnames/bind'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
import styles from './Highlight.module.scss'
|
||||
|
||||
interface Props {
|
||||
show: boolean
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const Highlight = (props: Props) => {
|
||||
const classes = classNames([
|
||||
styles.highlight,
|
||||
props.show ? styles.show : styles.hide,
|
||||
props.className,
|
||||
])
|
||||
return <div className={classes}>{props.children}</div>
|
||||
}
|
196
src/components/common/InputSection/InputSection.module.scss
Normal file
196
src/components/common/InputSection/InputSection.module.scss
Normal file
@ -0,0 +1,196 @@
|
||||
@import 'src/styles/master';
|
||||
|
||||
.container {
|
||||
border-radius: $borderRadiusL;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@include padding(0, 3);
|
||||
|
||||
.unstakeUpperInputInfo {
|
||||
align-self: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: rem-calc(420);
|
||||
margin-bottom: space(1);
|
||||
margin-top: space(4);
|
||||
max-width: 100%;
|
||||
|
||||
.spacer {
|
||||
width: rem-calc(20);
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: auto;
|
||||
opacity: 0.3;
|
||||
text-transform: none;
|
||||
|
||||
.block {
|
||||
display: inline-block;
|
||||
|
||||
&:first-child {
|
||||
margin-inline-end: space(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.xMarsRatio {
|
||||
text-align: end;
|
||||
}
|
||||
}
|
||||
|
||||
.available {
|
||||
align-self: center;
|
||||
@include margin(2, 0, 0);
|
||||
opacity: 0.3;
|
||||
@include typoXXS;
|
||||
border: none;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: $colorInfoVoteAgainst;
|
||||
align-self: center;
|
||||
align-content: center;
|
||||
margin-bottom: space(5);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-self: center;
|
||||
position: relative;
|
||||
|
||||
.input {
|
||||
align-self: center;
|
||||
display: flex;
|
||||
margin-bottom: space(5);
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sliderButton {
|
||||
color: $fontColorLightPrimary;
|
||||
opacity: 0.6;
|
||||
height: rem-calc(24);
|
||||
@include margin(1, 2, 0, 2);
|
||||
@include typoXS;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
word-break: keep-all;
|
||||
|
||||
&.zero {
|
||||
@include typoS;
|
||||
}
|
||||
|
||||
&.maxButton {
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inputWarning {
|
||||
position: absolute;
|
||||
top: rem-calc(-70);
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@include typoS;
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
align-self: center;
|
||||
opacity: 1;
|
||||
border: 1px solid $colorGreyHighlight;
|
||||
width: rem-calc(448);
|
||||
max-width: 100%;
|
||||
height: rem-calc(56);
|
||||
border-radius: $borderRadiusS;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.inputPercentage {
|
||||
text-align: center;
|
||||
opacity: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
color: $fontColorLightPrimary;
|
||||
@include typoXXL;
|
||||
|
||||
&::placeholder {
|
||||
text-indent: rem-calc(-14);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
input::-webkit-inner-spin-button {
|
||||
appearance: none;
|
||||
@include margin(0);
|
||||
}
|
||||
}
|
||||
|
||||
.inputRaw {
|
||||
align-self: center;
|
||||
background: none;
|
||||
margin-top: space(2);
|
||||
opacity: 0.4;
|
||||
margin-bottom: space(9);
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: space(3);
|
||||
}
|
||||
}
|
||||
|
||||
.feeTooltipContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.fee {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.reminder {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.content {
|
||||
margin-top: space(5);
|
||||
width: rem-calc(448);
|
||||
text-align: center;
|
||||
align-self: center;
|
||||
|
||||
.link {
|
||||
color: $colorPrimary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $bpMediumLow) {
|
||||
.container {
|
||||
.inputContainer {
|
||||
@include padding(0, 8);
|
||||
}
|
||||
|
||||
.inputWarning {
|
||||
top: rem-calc(-50);
|
||||
left: 20%;
|
||||
width: 60%;
|
||||
}
|
||||
}
|
||||
}
|
235
src/components/common/InputSection/InputSection.tsx
Normal file
235
src/components/common/InputSection/InputSection.tsx
Normal file
@ -0,0 +1,235 @@
|
||||
import { Button, DisplayCurrency, InputSlider } from 'components/common'
|
||||
import { formatValue, lookup, lookupSymbol, parseNumberInput } from 'libs/parse'
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import CurrencyInput from 'react-currency-input-field'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useStore from 'store'
|
||||
import { ViewType } from 'types/enums'
|
||||
|
||||
import styles from './InputSection.module.scss'
|
||||
|
||||
interface Props {
|
||||
asset: RedBankAsset
|
||||
availableText: string
|
||||
amount: number
|
||||
maxUsableAmount: number
|
||||
actionButton: ReactNode
|
||||
checkForMaxValue?: boolean
|
||||
disabled?: boolean
|
||||
amountUntilDepositCap: number
|
||||
activeView: ViewType
|
||||
inputCallback: (value: number) => void
|
||||
onEnterHandler: () => void
|
||||
setAmountCallback: (value: number) => void
|
||||
}
|
||||
|
||||
export const InputSection = ({
|
||||
asset,
|
||||
availableText,
|
||||
amount,
|
||||
maxUsableAmount,
|
||||
actionButton,
|
||||
checkForMaxValue = false,
|
||||
disabled,
|
||||
amountUntilDepositCap,
|
||||
activeView,
|
||||
inputCallback,
|
||||
onEnterHandler,
|
||||
setAmountCallback,
|
||||
}: Props) => {
|
||||
// ------------------
|
||||
// EXTERNAL HOOKS
|
||||
// ------------------
|
||||
const { t } = useTranslation()
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// ------------------
|
||||
// STORE STATE
|
||||
// ------------------
|
||||
const baseCurrency = useStore((s) => s.baseCurrency)
|
||||
const whitelistedAssets = useStore((s) => s.whitelistedAssets)
|
||||
|
||||
// ------------------
|
||||
// LOCAL STATE
|
||||
// ------------------
|
||||
const [sliderValue, setSliderValue] = useState(0)
|
||||
const [depositWarning, setDepositWarning] = useState(false)
|
||||
const [fakeAmount, setFakeAmount] = useState<string | undefined>()
|
||||
|
||||
// ------------------
|
||||
// VARIABLES
|
||||
// ------------------
|
||||
|
||||
const sliderEnabled = useMemo(() => maxUsableAmount > 1, [maxUsableAmount])
|
||||
|
||||
// If within 50ms of our last update for the slider, return the cached value.
|
||||
// This prevents a 'flicker' of the display label when we finish dragging while hovering
|
||||
const sliderDisplayValue = useMemo(() => {
|
||||
const amountToSet = (amount / maxUsableAmount) * 100
|
||||
return amountToSet >= 100 ? amountToSet + Math.random() : amountToSet
|
||||
}, [amount, maxUsableAmount])
|
||||
|
||||
// -----------------------
|
||||
// Callbacks
|
||||
// -----------------------
|
||||
|
||||
const onSliderDragged = useMemo(
|
||||
() => (value: number) => {
|
||||
const selectedValue = value / 100
|
||||
setAmountCallback(maxUsableAmount * selectedValue)
|
||||
if (checkForMaxValue) {
|
||||
setSliderValue(value)
|
||||
}
|
||||
}, // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[setAmountCallback, maxUsableAmount],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef?.current) inputRef.current.focus()
|
||||
}, [])
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (
|
||||
amount >= maxUsableAmount &&
|
||||
asset.denom === baseCurrency.denom &&
|
||||
!depositWarning &&
|
||||
checkForMaxValue
|
||||
) {
|
||||
setDepositWarning(true)
|
||||
} else {
|
||||
setDepositWarning(false)
|
||||
}
|
||||
}, // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[sliderValue, amount, maxUsableAmount],
|
||||
)
|
||||
|
||||
// -------------
|
||||
// Presentation
|
||||
// -------------
|
||||
|
||||
const produceUpperInputInfo = () => {
|
||||
return (
|
||||
<Button
|
||||
color='quaternary'
|
||||
className={`overline ${styles.available}`}
|
||||
onClick={() => setAmountCallback(maxUsableAmount)}
|
||||
text={availableText}
|
||||
variant='transparent'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const suffix = lookupSymbol(asset.denom, whitelistedAssets || []) || asset.denom
|
||||
|
||||
const produceWarningComponent = useCallback(() => {
|
||||
let text = ''
|
||||
switch (activeView) {
|
||||
case ViewType.Deposit:
|
||||
text =
|
||||
amountUntilDepositCap <= 0
|
||||
? t('redbank.warning.depositCapReached', {
|
||||
symbol: asset.symbol,
|
||||
})
|
||||
: amount > amountUntilDepositCap
|
||||
? t('redbank.warning.depositCap', {
|
||||
symbol: asset.symbol,
|
||||
amount: amountUntilDepositCap / 10 ** asset.decimals,
|
||||
})
|
||||
: ''
|
||||
break
|
||||
case ViewType.Withdraw:
|
||||
text = maxUsableAmount < 1 ? t('redbank.warning.withdraw') : ''
|
||||
break
|
||||
case ViewType.Borrow:
|
||||
text = maxUsableAmount < 1 ? t('redbank.warning.borrow') : ''
|
||||
break
|
||||
}
|
||||
|
||||
return <span className={`${styles.warning}`}>{text}</span>
|
||||
}, [activeView, amountUntilDepositCap, amount, asset, t, maxUsableAmount])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* INPUT SECTION */}
|
||||
<div className={styles.container}>
|
||||
{produceUpperInputInfo()}
|
||||
|
||||
<div className={styles.inputWrapper}>
|
||||
<CurrencyInput
|
||||
allowNegativeValue={false}
|
||||
autoFocus={true}
|
||||
className={`h4 number ${styles.inputPercentage}`}
|
||||
decimalSeparator='.'
|
||||
decimalsLimit={asset.decimals}
|
||||
disableAbbreviations={true}
|
||||
disabled={disabled}
|
||||
groupSeparator=','
|
||||
name='currencyInput'
|
||||
onKeyPress={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
onEnterHandler()
|
||||
}
|
||||
}}
|
||||
onValueChange={(value) => {
|
||||
if (value?.charAt(value.length - 1) !== '.') {
|
||||
inputCallback(parseNumberInput(value))
|
||||
setFakeAmount(undefined)
|
||||
} else {
|
||||
setFakeAmount(value)
|
||||
}
|
||||
}}
|
||||
placeholder='0'
|
||||
suffix={` ${suffix}`}
|
||||
value={
|
||||
fakeAmount
|
||||
? fakeAmount
|
||||
: amount
|
||||
? lookup(
|
||||
Number(formatValue(amount, 0, 0, false, false, false, false, false)),
|
||||
asset.denom,
|
||||
asset.decimals,
|
||||
)
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<DisplayCurrency
|
||||
className={`overline ${styles.inputRaw}`}
|
||||
coin={{ amount: amount.toString(), denom: asset.denom }}
|
||||
/>
|
||||
<div className={styles.inputContainer}>
|
||||
<button
|
||||
className={`${styles.sliderButton} ${styles.zero}`}
|
||||
disabled={disabled}
|
||||
onClick={() => setAmountCallback(0)}
|
||||
>
|
||||
0
|
||||
</button>
|
||||
<div className={styles.input}>
|
||||
<InputSlider
|
||||
disabled={disabled}
|
||||
enabled={sliderEnabled}
|
||||
onChange={onSliderDragged}
|
||||
value={sliderDisplayValue}
|
||||
/>
|
||||
{depositWarning && (
|
||||
<div className={styles.inputWarning}>
|
||||
<p className='tippyContainer'>{t('common.lowUstAmountAfterTransaction')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className={`caption ${styles.sliderButton}`}
|
||||
disabled={disabled}
|
||||
onClick={() => setAmountCallback(maxUsableAmount)}
|
||||
>
|
||||
{t('global.max')}
|
||||
</button>
|
||||
</div>
|
||||
{produceWarningComponent()}
|
||||
<div className={styles.actionButton}>{actionButton}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
79
src/components/common/InputSlider/InputSlider.module.scss
Normal file
79
src/components/common/InputSlider/InputSlider.module.scss
Normal file
@ -0,0 +1,79 @@
|
||||
@import 'src/styles/master';
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
align-self: center;
|
||||
|
||||
.leverageLimit {
|
||||
position: relative;
|
||||
@include margin(0, 3);
|
||||
|
||||
.line {
|
||||
position: absolute;
|
||||
height: rem-calc(24);
|
||||
width: rem-calc(1);
|
||||
background: $alphaWhite60;
|
||||
}
|
||||
|
||||
.outside {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: rem-calc(12);
|
||||
height: rem-calc(4);
|
||||
border-radius: $borderRadiusM;
|
||||
@include bgHatched;
|
||||
}
|
||||
}
|
||||
|
||||
.labelComponentContainer {
|
||||
position: relative;
|
||||
.labelComponent {
|
||||
@include typoS;
|
||||
@include layoutTooltip;
|
||||
color: $fontColorLightPrimary;
|
||||
margin-inline-start: space(-9);
|
||||
margin-top: space(5);
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
|
||||
span {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
@include padding(0, 3);
|
||||
}
|
||||
|
||||
.labels {
|
||||
@include margin(1, 0, 0, 0);
|
||||
color: $alphaWhite60;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
height: rem-calc(16);
|
||||
overflow: hidden;
|
||||
|
||||
> * {
|
||||
width: rem-calc(25);
|
||||
text-align: left;
|
||||
word-break: keep-all;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.overLeveraged {
|
||||
width: rem-calc(32);
|
||||
text-align: right;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $bpMediumHigh) {
|
||||
.container {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
291
src/components/common/InputSlider/InputSlider.tsx
Normal file
291
src/components/common/InputSlider/InputSlider.tsx
Normal file
@ -0,0 +1,291 @@
|
||||
import Slider from '@material-ui/core/Slider'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import { getLeverageRatio } from 'functions/fields'
|
||||
import { formatValue, roundToDecimals } from 'libs/parse'
|
||||
import throttle from 'lodash.throttle'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import colors from 'styles/_assets.module.scss'
|
||||
|
||||
import styles from './InputSlider.module.scss'
|
||||
|
||||
interface Props {
|
||||
value: number
|
||||
enabled: boolean
|
||||
sliderColor?: string
|
||||
isLeverage?: boolean
|
||||
errorLabel?: string
|
||||
minValue?: number
|
||||
maxValue?: number
|
||||
customMark?: number
|
||||
lastUpdate?: number
|
||||
showError?: boolean
|
||||
disabled?: boolean
|
||||
leverageLimit?: number
|
||||
leverageMax?: number
|
||||
onChange: (value: number) => void
|
||||
}
|
||||
|
||||
interface Marks {
|
||||
value: number
|
||||
label?: string
|
||||
}
|
||||
|
||||
export const InputSlider = ({
|
||||
value,
|
||||
onChange,
|
||||
enabled,
|
||||
sliderColor = colors.primary,
|
||||
isLeverage = false,
|
||||
minValue = 0,
|
||||
maxValue = 100,
|
||||
customMark = 0,
|
||||
disabled = false,
|
||||
leverageMax = 2,
|
||||
leverageLimit = leverageMax,
|
||||
}: Props) => {
|
||||
const leveragePerPercent = (maxValue - 1) / 100
|
||||
const { t } = useTranslation()
|
||||
// ---------------------
|
||||
// Callbacks
|
||||
// ---------------------
|
||||
const inputUpdate = (percentage: number) => {
|
||||
if (percentage > 100) percentage = 100
|
||||
if (percentage < 0) percentage = 0
|
||||
if (isLeverage) {
|
||||
const maxAllowedPercentage = getLeverageRatio(leverageLimit, maxValue)
|
||||
if (percentage > maxAllowedPercentage) percentage = maxAllowedPercentage
|
||||
onChange(percentage * leveragePerPercent + 1)
|
||||
} else {
|
||||
onChange(percentage)
|
||||
}
|
||||
}
|
||||
|
||||
const throttledUpdate = throttle(inputUpdate, 100, { trailing: true })
|
||||
|
||||
// -------------------------
|
||||
// Presentation
|
||||
// -------------------------
|
||||
const leverageRatio = getLeverageRatio(leverageMax, maxValue)
|
||||
const marksArray =
|
||||
minValue === 0 && maxValue === 100
|
||||
? [customMark, 0, 25, 50, 75, 100]
|
||||
: [
|
||||
0,
|
||||
0.25 * leverageRatio,
|
||||
0.5 * leverageRatio,
|
||||
0.75 * leverageRatio,
|
||||
leverageRatio,
|
||||
...(100 - leverageRatio >= 2 ? [100] : []),
|
||||
]
|
||||
const marksPushed: any[] = []
|
||||
const marks: Marks[] = []
|
||||
for (let i = 0; i < marksArray.length; i++) {
|
||||
if (marksArray[i] === customMark && customMark !== 0) {
|
||||
const currentMark = {
|
||||
value: customMark,
|
||||
label: t('fields.current'),
|
||||
}
|
||||
if (customMark > 7 && customMark < 80 && !marksPushed.includes(marksArray[i])) {
|
||||
marks.push(currentMark)
|
||||
marksPushed.push(marksArray[i])
|
||||
}
|
||||
} else if (marksArray[i] === minValue && minValue !== 0) {
|
||||
const minMark = {
|
||||
value: minValue,
|
||||
label: t('global.min_lower'),
|
||||
}
|
||||
if (
|
||||
minValue > 5 &&
|
||||
minValue < 95 &&
|
||||
Math.abs(customMark - minValue) > 8 &&
|
||||
!marksPushed.includes(marksArray[i])
|
||||
) {
|
||||
marks.push(minMark)
|
||||
marksPushed.push(marksArray[i])
|
||||
}
|
||||
} else if (marksArray[i] === maxValue && maxValue !== 100) {
|
||||
const maxMark = {
|
||||
value: maxValue,
|
||||
label: t('global.max_lower'),
|
||||
}
|
||||
if (
|
||||
maxValue > 11 &&
|
||||
maxValue < 86 &&
|
||||
Math.abs(customMark - maxValue) > 8 &&
|
||||
!marksPushed.includes(marksArray[i])
|
||||
) {
|
||||
marks.push(maxMark)
|
||||
marksPushed.push(marksArray[i])
|
||||
}
|
||||
} else {
|
||||
const labelText = formatValue(
|
||||
((maxValue - 1) * marksArray[i]) / 100 + 1,
|
||||
2,
|
||||
2,
|
||||
false,
|
||||
false,
|
||||
'x',
|
||||
)
|
||||
// Labels only for the 1st, 3rd, 5th mark. In case of overleverage, display the 6th mark when 10% space is available
|
||||
const label = isLeverage
|
||||
? [0, 2, 4].includes(i) || (i === 5 && marksArray[4] / marksArray[5] < 0.9)
|
||||
? labelText
|
||||
: ''
|
||||
: ''
|
||||
|
||||
const markObject = {
|
||||
value: marksArray[i],
|
||||
label,
|
||||
}
|
||||
if (!marksPushed.includes(marksArray[i])) {
|
||||
marks.push(markObject)
|
||||
marksPushed.push(marksArray[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MarsSlider = withStyles({
|
||||
root: {
|
||||
color: sliderColor,
|
||||
height: 8,
|
||||
},
|
||||
thumb: {
|
||||
height: 24,
|
||||
width: 24,
|
||||
backgroundColor: colors.white,
|
||||
marginTop: -12,
|
||||
marginLeft: -13,
|
||||
boxShadow:
|
||||
'0px 3px 4px rgba(0, 0, 0, 0.14), 0px 3px 3px rgba(0, 0, 0, 0.12), 0px 1px 8px rgba(0, 0, 0, 0.2)',
|
||||
'&:hover, &$active': {
|
||||
boxShadow: `${sliderColor} 0 2px 3px 1px`,
|
||||
display: 'flex',
|
||||
},
|
||||
'& .bar': {
|
||||
height: 9,
|
||||
width: 1,
|
||||
backgroundColor: sliderColor,
|
||||
marginLeft: 1,
|
||||
marginRight: 1,
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
color: sliderColor,
|
||||
'& .MuiSlider-thumb': {
|
||||
height: 24,
|
||||
width: 24,
|
||||
marginTop: -12,
|
||||
marginLeft: -13,
|
||||
backgroundColor: colors.greyLight,
|
||||
},
|
||||
'& .MuiSlider-track': {
|
||||
color: sliderColor,
|
||||
opacity: 0.2,
|
||||
},
|
||||
},
|
||||
active: {},
|
||||
track: {
|
||||
height: 4,
|
||||
borderRadius: 6,
|
||||
opacity: 1.0,
|
||||
boxShadow: `0px 0px 6px ${sliderColor}`,
|
||||
},
|
||||
rail: {
|
||||
height: 4,
|
||||
borderRadius: 6,
|
||||
background: 'rgba(191,191,191,0.18)',
|
||||
opacity: 1,
|
||||
},
|
||||
mark: {
|
||||
backgroundColor: colors.greyLight,
|
||||
color: colors.white,
|
||||
height: 8,
|
||||
width: 8,
|
||||
marginLeft: -4,
|
||||
marginTop: 12,
|
||||
borderRadius: 4,
|
||||
opacity: 0.6,
|
||||
},
|
||||
markLabel: {
|
||||
color: colors.greyMedium,
|
||||
opacity: 0.6,
|
||||
},
|
||||
markActive: {
|
||||
opacity: 1,
|
||||
backgroundColor: 'sliderColor',
|
||||
},
|
||||
})(Slider)
|
||||
|
||||
const thumbComponent = (props: any) => {
|
||||
return (
|
||||
<span {...props}>
|
||||
<span key={0} className='bar' />
|
||||
<span key={1} className='bar' />
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const valueLabelComponent = (props: any) => {
|
||||
const { children, open, value } = props
|
||||
const currentLeverage = Number(value.replace('x', ''))
|
||||
const offset = !isLeverage ? value : `${((currentLeverage - 1) / (maxValue - 1)) * 100 + 2}%`
|
||||
|
||||
return (
|
||||
<div className={styles.labelComponentContainer}>
|
||||
{children}
|
||||
{open ? (
|
||||
<div
|
||||
className={styles.labelComponent}
|
||||
style={{
|
||||
left: offset,
|
||||
}}
|
||||
>
|
||||
<span className={styles.valueLabel}>{value}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const valueLabel = (x: any) => {
|
||||
const ceiled = Math.ceil(x) || 0
|
||||
if (!isLeverage) {
|
||||
return `${ceiled}%`
|
||||
} else {
|
||||
const s = roundToDecimals(new BigNumber(x).times(leveragePerPercent).plus(1).toNumber(), 2)
|
||||
return `${s.toFixed(2)}x`
|
||||
}
|
||||
}
|
||||
|
||||
const percentage = getLeverageRatio(leverageLimit, maxValue) || 0
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{isLeverage && leverageLimit < leverageMax && (
|
||||
<div className={styles.leverageLimit}>
|
||||
<div className={styles.line} style={{ left: `calc(${percentage}% - 1px)` }}></div>
|
||||
<div className={styles.outside} style={{ left: `calc(${percentage}% + 1px)` }}></div>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.slider}>
|
||||
{enabled ? (
|
||||
<MarsSlider
|
||||
ThumbComponent={thumbComponent}
|
||||
ValueLabelComponent={valueLabelComponent}
|
||||
defaultValue={getLeverageRatio(value, maxValue)}
|
||||
disabled={disabled}
|
||||
step={0.5}
|
||||
valueLabelDisplay={'auto'}
|
||||
valueLabelFormat={valueLabel}
|
||||
marks={marks}
|
||||
onChangeCommitted={(e, percentage) => throttledUpdate(Math.round(Number(percentage)))}
|
||||
/>
|
||||
) : (
|
||||
<MarsSlider disabled={true} marks={marks} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
57
src/components/common/Layout/Layout.tsx
Normal file
57
src/components/common/Layout/Layout.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { useWallet, WalletConnectionStatus } from '@marsprotocol/wallet-connector'
|
||||
import classNames from 'classnames'
|
||||
import { Footer, Header, MobileNav } from 'components/common'
|
||||
import { FieldsNotConnected } from 'components/fields'
|
||||
import { RedbankNotConnected } from 'components/redbank'
|
||||
import { SESSION_WALLET_KEY } from 'constants/appConstants'
|
||||
import { useAnimations } from 'hooks/data'
|
||||
import { useRouter } from 'next/router'
|
||||
import React, { useEffect } from 'react'
|
||||
import useStore from 'store'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const Layout = ({ children }: Props) => {
|
||||
const router = useRouter()
|
||||
useAnimations()
|
||||
|
||||
const userWalletAddress = useStore((s) => s.userWalletAddress)
|
||||
const enableAnimations = useStore((s) => s.enableAnimations)
|
||||
const backgroundClasses = classNames('background', !userWalletAddress && 'night')
|
||||
const vaultConfigs = useStore((s) => s.vaultConfigs)
|
||||
const wallet = useWallet()
|
||||
const wasConnectedBefore = !!localStorage.getItem(SESSION_WALLET_KEY)
|
||||
const connectionSuccess = !!(
|
||||
(wallet.status === WalletConnectionStatus.Connecting && wasConnectedBefore) ||
|
||||
wallet.status === WalletConnectionStatus.Connected
|
||||
)
|
||||
const isConnected = !!userWalletAddress || connectionSuccess
|
||||
|
||||
useEffect(() => {
|
||||
if (!userWalletAddress) {
|
||||
useStore.setState({ availableVaults: vaultConfigs, activeVaults: [] })
|
||||
}
|
||||
}, [userWalletAddress, vaultConfigs])
|
||||
|
||||
return (
|
||||
<div className={classNames('app', !enableAnimations && 'no-motion')}>
|
||||
<div className={backgroundClasses} id='bg' />
|
||||
<Header />
|
||||
<div className='appContainer'>
|
||||
<div className='widthBox'>
|
||||
{isConnected ? (
|
||||
<div className={'body'}>{children}</div>
|
||||
) : router.route.includes('farm') ? (
|
||||
<FieldsNotConnected />
|
||||
) : (
|
||||
<RedbankNotConnected />
|
||||
)}
|
||||
</div>
|
||||
<Footer />
|
||||
<MobileNav />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
51
src/components/common/MobileNav/MobileNav.module.scss
Normal file
51
src/components/common/MobileNav/MobileNav.module.scss
Normal file
@ -0,0 +1,51 @@
|
||||
@import 'src/styles/master';
|
||||
|
||||
.mobileNav {
|
||||
display: flex;
|
||||
flex: 0 0 100%;
|
||||
width: 100%;
|
||||
height: $mobileNavHeight;
|
||||
background-color: $alphaBlack30;
|
||||
backdrop-filter: blur(16px);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
z-index: 55;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
@include padding(0, 4);
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 0 auto;
|
||||
width: rem-calc(100);
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
@include margin(-8, 0, 0);
|
||||
filter: grayscale(1);
|
||||
opacity: 0.4;
|
||||
|
||||
&.active {
|
||||
filter: grayscale(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
span {
|
||||
@include padding(1, 0, 0);
|
||||
color: $fontColorLightPrimary;
|
||||
@include typoXScaps;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: rem-calc(50);
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $bpMediumLow) {
|
||||
.mobileNav {
|
||||
display: none;
|
||||
}
|
||||
}
|
35
src/components/common/MobileNav/MobileNav.tsx
Normal file
35
src/components/common/MobileNav/MobileNav.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import classNames from 'classnames'
|
||||
import { SVG } from 'components/common'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useStore from 'store'
|
||||
|
||||
import styles from './MobileNav.module.scss'
|
||||
|
||||
export const MobileNav = () => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const networkConfig = useStore((s) => s.networkConfig)
|
||||
|
||||
return (
|
||||
<nav className={styles.mobileNav}>
|
||||
<Link href='/redbank' passHref>
|
||||
<a className={classNames(styles.nav, !router.pathname.includes('farm') && styles.active)}>
|
||||
<SVG.RedBankIcon />
|
||||
<span>{t('global.redBank')}</span>
|
||||
</a>
|
||||
</Link>
|
||||
<a className={styles.nav} target='_blank' href={networkConfig?.councilUrl} rel='noreferrer'>
|
||||
<SVG.CouncilIcon />
|
||||
<span>{t('global.council')}</span>
|
||||
</a>
|
||||
<Link href='/farm' passHref>
|
||||
<a className={classNames(styles.nav, router.pathname.includes('farm') && styles.active)}>
|
||||
<SVG.FieldsIcon />
|
||||
<span>{t('global.fields')}</span>
|
||||
</a>
|
||||
</Link>
|
||||
</nav>
|
||||
)
|
||||
}
|
77
src/components/common/Notification/Notification.module.scss
Normal file
77
src/components/common/Notification/Notification.module.scss
Normal file
@ -0,0 +1,77 @@
|
||||
@import 'src/styles/master';
|
||||
|
||||
.notification {
|
||||
width: 100%;
|
||||
min-height: rem-calc(48);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include layoutPopover;
|
||||
margin-bottom: space(8) !important;
|
||||
@include padding(2, 3);
|
||||
position: relative;
|
||||
justify-content: flex-start;
|
||||
|
||||
p {
|
||||
@include margin(0);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
@include padding(2, 0);
|
||||
}
|
||||
|
||||
.icon {
|
||||
@include margin(0, 4);
|
||||
}
|
||||
|
||||
&.info {
|
||||
color: $colorAccent;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: $colorWhite;
|
||||
font-weight: $fontWeightRegular;
|
||||
background: linear-gradient(0deg, $colorInfoWarning, $colorInfoLoss);
|
||||
|
||||
.closeNotification {
|
||||
color: $colorWhite;
|
||||
}
|
||||
}
|
||||
|
||||
&.closeBtnSpace {
|
||||
@include padding(0, 10, 0, 0);
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
@include margin(0, 1);
|
||||
color: $colorSecondary;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.closeNotification {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
@include margin(4, 4, 0, 0);
|
||||
border: none;
|
||||
background: transparent;
|
||||
@include padding(0);
|
||||
opacity: 0.6;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: rem-calc(14);
|
||||
height: rem-calc(13);
|
||||
}
|
||||
}
|
||||
}
|
50
src/components/common/Notification/Notification.tsx
Normal file
50
src/components/common/Notification/Notification.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import classNames from 'classnames/bind'
|
||||
import { SVG } from 'components/common'
|
||||
import { ReactNode, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { NotificationType } from 'types/enums'
|
||||
|
||||
import styles from './Notification.module.scss'
|
||||
|
||||
interface Props {
|
||||
showNotification: boolean
|
||||
content: ReactNode
|
||||
type: NotificationType
|
||||
hideCloseBtn?: boolean
|
||||
}
|
||||
|
||||
export const Notification = ({ showNotification, content, type, hideCloseBtn = false }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [closeNotification, setCloseNotification] = useState(false)
|
||||
const classes = classNames.bind(styles)
|
||||
|
||||
const notificationClasses = classes({
|
||||
notification: true,
|
||||
closeBtnSpace: !hideCloseBtn,
|
||||
info: type === NotificationType.Info,
|
||||
warning: type === NotificationType.Warning,
|
||||
})
|
||||
|
||||
if (!showNotification || closeNotification) return <></>
|
||||
|
||||
return (
|
||||
<div className={notificationClasses}>
|
||||
<span className={styles.icon}>
|
||||
{type === NotificationType.Warning ? <SVG.Warning /> : <SVG.Info />}
|
||||
</span>
|
||||
<div className={styles.content}>{content}</div>
|
||||
|
||||
{!hideCloseBtn && (
|
||||
<button
|
||||
className={styles.closeNotification}
|
||||
onClick={() => {
|
||||
setCloseNotification(true)
|
||||
}}
|
||||
title={t('common.close')}
|
||||
>
|
||||
<SVG.SmallClose />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
18
src/components/common/NumberInput/NumberInput.module.scss
Normal file
18
src/components/common/NumberInput/NumberInput.module.scss
Normal file
@ -0,0 +1,18 @@
|
||||
@import 'src/styles/master';
|
||||
|
||||
.input {
|
||||
text-align: left;
|
||||
background: none;
|
||||
border: none;
|
||||
appearance: none;
|
||||
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
@include typoS;
|
||||
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
132
src/components/common/NumberInput/NumberInput.tsx
Normal file
132
src/components/common/NumberInput/NumberInput.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
import { lookup } from 'libs/parse'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import styles from './NumberInput.module.scss'
|
||||
|
||||
interface Props {
|
||||
value: string
|
||||
className: string
|
||||
maxDecimals: number
|
||||
maxValue?: number
|
||||
maxLength?: number
|
||||
suffix?: string
|
||||
onChange: (value: number) => void
|
||||
onBlur?: () => void
|
||||
onFocus?: () => void
|
||||
onRef?: (ref: React.RefObject<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
export const NumberInput = (props: Props) => {
|
||||
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||
const cursorRef = React.useRef(0)
|
||||
const [inputValue, setInputValue] = useState({
|
||||
formatted: props.value,
|
||||
value: Number(props.value),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue({
|
||||
formatted: props.value,
|
||||
value: Number(props.value),
|
||||
})
|
||||
}, [props.value])
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.onRef) return
|
||||
props.onRef(inputRef)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [inputRef, props.onRef])
|
||||
|
||||
const onInputFocus = () => {
|
||||
inputRef.current?.select()
|
||||
props.onFocus && props.onFocus()
|
||||
}
|
||||
|
||||
const updateValues = (formatted: string, value: number) => {
|
||||
const lastChar = formatted.charAt(formatted.length - 1)
|
||||
if (lastChar === '.') {
|
||||
cursorRef.current = (inputRef.current?.selectionEnd || 0) + 1
|
||||
} else {
|
||||
cursorRef.current = inputRef.current?.selectionEnd || 0
|
||||
}
|
||||
setInputValue({ formatted, value })
|
||||
if (value !== inputValue.value) {
|
||||
props.onChange(value)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!inputRef.current) return
|
||||
|
||||
const cursor = cursorRef.current
|
||||
const length = inputValue.formatted.length
|
||||
|
||||
if (cursor > length) {
|
||||
inputRef.current.setSelectionRange(length, length)
|
||||
return
|
||||
}
|
||||
|
||||
inputRef.current.setSelectionRange(cursor, cursor)
|
||||
}, [inputValue, inputRef])
|
||||
|
||||
const onInputChange = (value: string) => {
|
||||
if (props.suffix) {
|
||||
value = value.replace(props.suffix, '')
|
||||
}
|
||||
const numberCount = value.match(/[0-9]/g)?.length || 0
|
||||
const decimals = value.split('.')[1]?.length || 0
|
||||
const lastChar = value.charAt(value.length - 1)
|
||||
const isNumber = !isNaN(Number(value))
|
||||
const hasMultipleDots = (value.match(/[.,]/g)?.length || 0) > 1
|
||||
const isSeparator = lastChar === '.' || lastChar === ','
|
||||
|
||||
if (isSeparator && value.length === 1) {
|
||||
updateValues('0.', 0)
|
||||
return
|
||||
}
|
||||
|
||||
if (isSeparator && !hasMultipleDots) {
|
||||
updateValues(value.replace(',', '.'), inputValue.value)
|
||||
return
|
||||
}
|
||||
|
||||
if (!isNumber) return
|
||||
if (hasMultipleDots) return
|
||||
|
||||
if (props.maxDecimals !== undefined && decimals > props.maxDecimals) {
|
||||
value = value.substring(0, value.length - 1)
|
||||
}
|
||||
|
||||
if (props.maxLength !== undefined && numberCount > props.maxLength) return
|
||||
|
||||
if ((props.maxValue && Number(value) > props.maxValue) || props.maxValue === 0) {
|
||||
updateValues(String(props.maxValue), props.maxValue)
|
||||
return
|
||||
}
|
||||
|
||||
if (lastChar === '0' && Number(value) === Number(inputValue.value)) {
|
||||
cursorRef.current = (inputRef.current?.selectionEnd || 0) + 1
|
||||
setInputValue({ ...inputValue, formatted: value })
|
||||
return
|
||||
}
|
||||
|
||||
if (value === '') {
|
||||
updateValues(value, 0)
|
||||
return
|
||||
}
|
||||
|
||||
updateValues(String(lookup(Number(value), '', 6)), Number(value))
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type='text'
|
||||
value={`${inputValue.formatted}${props.suffix ? props.suffix : ''}`}
|
||||
onFocus={onInputFocus}
|
||||
onChange={(e) => onInputChange(e.target.value)}
|
||||
onBlur={props.onBlur}
|
||||
className={`${props.className} ${styles.input}`}
|
||||
/>
|
||||
)
|
||||
}
|
8
src/components/common/SVG/Add.tsx
Normal file
8
src/components/common/SVG/Add.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
export const Add = () => (
|
||||
<svg width='12' height='12' viewBox='0 0 12 12' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M10.6667 5.33317H6.66675V1.33317C6.66675 1.15636 6.59651 0.98679 6.47149 0.861766C6.34646 0.736742 6.17689 0.666504 6.00008 0.666504C5.82327 0.666504 5.6537 0.736742 5.52868 0.861766C5.40365 0.98679 5.33341 1.15636 5.33341 1.33317V5.33317H1.33341C1.1566 5.33317 0.987035 5.40341 0.86201 5.52843C0.736986 5.65346 0.666748 5.82303 0.666748 5.99984C0.666748 6.17665 0.736986 6.34622 0.86201 6.47124C0.987035 6.59627 1.1566 6.6665 1.33341 6.6665H5.33341V10.6665C5.33341 10.8433 5.40365 11.0129 5.52868 11.1379C5.6537 11.2629 5.82327 11.3332 6.00008 11.3332C6.17689 11.3332 6.34646 11.2629 6.47149 11.1379C6.59651 11.0129 6.66675 10.8433 6.66675 10.6665V6.6665H10.6667C10.8436 6.6665 11.0131 6.59627 11.1382 6.47124C11.2632 6.34622 11.3334 6.17665 11.3334 5.99984C11.3334 5.82303 11.2632 5.65346 11.1382 5.52843C11.0131 5.40341 10.8436 5.33317 10.6667 5.33317Z'
|
||||
fill='white'
|
||||
/>
|
||||
</svg>
|
||||
)
|
23
src/components/common/SVG/Apollo.tsx
Normal file
23
src/components/common/SVG/Apollo.tsx
Normal file
File diff suppressed because one or more lines are too long
24
src/components/common/SVG/ArrowBack.tsx
Normal file
24
src/components/common/SVG/ArrowBack.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
export const ArrowBack = ({ color }: SVGProps) => {
|
||||
return (
|
||||
<svg
|
||||
height='48'
|
||||
version='1.1'
|
||||
viewBox='0 0 48 48'
|
||||
width='48'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<circle
|
||||
cx='24'
|
||||
cy='24'
|
||||
fill='none'
|
||||
r='23.5'
|
||||
stroke={color || 'currentColor'}
|
||||
transform='rotate(180 24 24)'
|
||||
/>
|
||||
<path
|
||||
d='M32 25C32.5523 25 33 24.5523 33 24C33 23.4477 32.5523 23 32 23L32 25ZM15.2929 23.2929C14.9024 23.6834 14.9024 24.3166 15.2929 24.7071L21.6569 31.0711C22.0474 31.4616 22.6805 31.4616 23.0711 31.0711C23.4616 30.6805 23.4616 30.0474 23.0711 29.6569L17.4142 24L23.0711 18.3431C23.4616 17.9526 23.4616 17.3195 23.0711 16.9289C22.6805 16.5384 22.0474 16.5384 21.6569 16.9289L15.2929 23.2929ZM32 23L16 23L16 25L32 25L32 23Z'
|
||||
fill={color || 'currentColor'}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
7
src/components/common/SVG/ArrowDown.tsx
Normal file
7
src/components/common/SVG/ArrowDown.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
export const ArrowDown = ({ color = '#FFFFFF' }: SVGProps) => {
|
||||
return (
|
||||
<svg version='1.1' viewBox='0 0 8 6' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path d='M4 6L0.535899 -1.75695e-07L7.4641 4.29987e-07L4 6Z' fill={color} />
|
||||
</svg>
|
||||
)
|
||||
}
|
30
src/components/common/SVG/Atom.tsx
Normal file
30
src/components/common/SVG/Atom.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
export const Atom = ({ color = '#FFFFFF' }: SVGProps) => {
|
||||
return (
|
||||
<svg
|
||||
data-name='Layer 1'
|
||||
id='Layer_1'
|
||||
viewBox='0 0 2500 2500'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<title>cosmos-atom-logo</title>
|
||||
<circle cx='1250' cy='1250' fill='#2e3148' r='1250' />
|
||||
<circle cx='1250' cy='1250' fill='#1b1e36' r='725.31' />
|
||||
<path
|
||||
d='M1252.57,159.47c-134.93,0-244.34,489.4-244.34,1093.11s109.41,1093.11,244.34,1093.11,244.34-489.4,244.34-1093.11S1387.5,159.47,1252.57,159.47ZM1269.44,2284c-15.43,20.58-30.86,5.14-30.86,5.14-62.14-72-93.21-205.76-93.21-205.76-108.69-349.79-82.82-1100.82-82.82-1100.82,51.08-596.24,144-737.09,175.62-768.36a19.29,19.29,0,0,1,24.74-2c45.88,32.51,84.36,168.47,84.36,168.47,113.63,421.81,103.34,817.9,103.34,817.9,10.29,344.65-56.94,730.45-56.94,730.45C1341.92,2222.22,1269.44,2284,1269.44,2284Z'
|
||||
fill='#6f7390'
|
||||
/>
|
||||
<path
|
||||
d='M2200.72,708.59c-67.18-117.08-546.09,31.58-1070,332s-893.47,638.89-826.34,755.92,546.09-31.58,1070-332,893.47-638.89,826.34-755.92h0ZM366.36,1780.45c-25.72-3.24-19.91-24.38-19.91-24.38C378,1666.36,478.4,1572.84,478.4,1572.84c249.43-268.36,913.79-619.65,913.79-619.65,542.54-252.42,711.06-241.77,753.81-230a19.29,19.29,0,0,1,14,20.58c-5.14,56-104.17,157-104.17,157C1746.71,1209.36,1398,1397.58,1398,1397.58c-293.83,180.5-661.93,314.09-661.93,314.09-280.09,100.93-369.7,68.78-369.7,68.78h0Z'
|
||||
fill='#6f7390'
|
||||
/>
|
||||
<path
|
||||
d='M2198.35,1800.41c67.7-116.77-300.93-456.79-823-759.47S374.43,587.76,306.79,704.73s300.93,456.79,823.3,759.47S2130.71,1917.39,2198.35,1800.41ZM351.65,749.85c-10-23.71,11.11-29.42,11.11-29.42C456.22,702.78,587.5,743,587.5,743c357.15,81.33,994,480.25,994,480.25,490.33,343.11,565.53,494.24,576.8,537.14a19.29,19.29,0,0,1-10.7,22.43c-51.13,23.41-188.07-11.47-188.07-11.47-422.07-113.17-759.62-320.52-759.62-320.52-303.29-163.58-603.19-415.28-603.19-415.28-227.88-191.87-245-285.44-245-285.44Z'
|
||||
fill='#6f7390'
|
||||
/>
|
||||
<circle cx='1250' cy='1250' fill='#b7b9c8' r='128.6' />
|
||||
<ellipse cx='1777.26' cy='756.17' fill='#b7b9c8' rx='74.59' ry='77.16' />
|
||||
<ellipse cx='552.98' cy='1018.52' fill='#b7b9c8' rx='74.59' ry='77.16' />
|
||||
<ellipse cx='1098.25' cy='1965.02' fill='#b7b9c8' rx='74.59' ry='77.16' />
|
||||
</svg>
|
||||
)
|
||||
}
|
9
src/components/common/SVG/BurgerMenu.tsx
Normal file
9
src/components/common/SVG/BurgerMenu.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
export const BurgerMenu = ({ color = '#FFFFFF' }: SVGProps) => {
|
||||
return (
|
||||
<svg version='1.1' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'>
|
||||
<line stroke={color} x1='3' x2='21' y1='12' y2='12' />
|
||||
<line stroke={color} x1='3' x2='21' y1='6' y2='6' />
|
||||
<line stroke={color} x1='3' x2='21' y1='18' y2='18' />
|
||||
</svg>
|
||||
)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user