This commit is contained in:
bwvdhelm 2023-01-31 16:46:56 +01:00
parent 37309b46fb
commit 5f019f3fdd
No known key found for this signature in database
GPG Key ID: 59FC90B476A8CB39
785 changed files with 44966 additions and 77798 deletions

12
.eslintrc.json Normal file
View 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
View File

@ -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

1
.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

View File

@ -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
View 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
View 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
View 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 Licensors 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 Departments 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 Licensees 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 Licensors 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.

View File

@ -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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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>

View File

@ -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
View 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
View 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
View 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',
})

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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;
}
}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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);
}
}
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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

View File

@ -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);
}
}
}

View File

@ -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

View File

@ -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);
}
}
}

View File

@ -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

View File

@ -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);
}
}

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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;
}
}

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -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

View File

@ -1,5 +0,0 @@
@use '../../styles/master' as *;
.container {
@include layoutTile;
}

View File

@ -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

View 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'

View 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;
}
}

View 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} />
}

View 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>
)
}

View 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);
}
}
}

View 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>
)
}

View 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;
}

View 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'

View 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);
}
}
}
}

View 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>
)
}

View File

@ -0,0 +1,11 @@
@import 'src/styles/master';
.noBalanceText {
display: none;
}
@media only screen and (min-width: 460px) {
.noBalanceText {
display: block;
}
}

View 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>
)
}

View 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);
}
}
}

View 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>
)
}

View File

@ -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);
}
}

View 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>
)
}

View File

@ -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;
}
}
}
}
}

View 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
}

View 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}</>
}

View 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}</>
}

View File

@ -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;
}
}
}

View File

@ -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>
)
}

View 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>
)
}

View 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%;
}
}
}
}

View 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>
)
}

View 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 />
}

View 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;
}
}
}

View 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>
)
}

View 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;
}
}
}
}
}

View 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>
)
}

View 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);
}
}
}

View 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>
)
}

View 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%;
}
}

View 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>
)
}

View 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;
}
}

View 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>
)
}

View 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;
}
}

View 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>
}

View 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%;
}
}
}

View 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>
)
}

View 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%;
}
}

View 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>
)
}

View 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>
)
}

View 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;
}
}

View 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>
)
}

View 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);
}
}
}

View 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>
)
}

View 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;
}
}

View 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}`}
/>
)
}

View 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>
)

File diff suppressed because one or more lines are too long

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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