diff --git a/.env b/.env new file mode 100644 index 000000000..0157db51e --- /dev/null +++ b/.env @@ -0,0 +1 @@ +SASS_PATH=node_modules:src/styles diff --git a/.env.development b/.env.development new file mode 100644 index 000000000..7d4ff5faa --- /dev/null +++ b/.env.development @@ -0,0 +1,3 @@ +HTTPS=true +HOST=local.mirror.finance +BROWSER=none diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..8c58cd190 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +.vscode +.vercel +.eslintcache \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 000000000..aa807423a --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# Mirror Web App (Terra) + +![Banner](banner.png) + +**NOTE**: This repository contains the source code for the Terra Mirror Web App, located at https://terra.mirror.finance. For the Ethereum version, visit [here](https://github.com/mirror-protocol/terra-web-app). + +The Mirror Web App is a web frontend for interacting with [Mirror Contracts](https://github.com/Mirror-Protocol/mirror-contracts). It is intended to be used with the [Terra Station Extension](https://terra.money/extension) plugin for Chromium browsers. + +## User Guide + +A detailed manual on how to perform various operations through the Mirror Web App are available on the [official docs site](https://docs.mirror.finance/user-guide/getting-started). + +## Development + +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +In the project directory, you can run: + +### `yarn start` + +Runs the app in the development mode.
+Open [https://localhost:3000](https://localhost:3000) to view it in the browser. + +## License + +This software is licensed under the Apache 2.0 license. Read more about it [here](./LICENSE). + +© 2020 Mirror Protocol diff --git a/banner.png b/banner.png new file mode 100644 index 000000000..e57801503 Binary files /dev/null and b/banner.png differ diff --git a/package.json b/package.json new file mode 100644 index 000000000..49d5770a4 --- /dev/null +++ b/package.json @@ -0,0 +1,88 @@ +{ + "name": "mirror", + "version": "1.0.0", + "homepage": "https://mirror.finance", + "repository": "github:Mirror-Protocol/mirror-web-app", + "author": "Terra ", + "license": "Apache-2.0", + "dependencies": { + "@apollo/client": "^3.2.9", + "@material-ui/core": "^4.11.1", + "@sentry/react": "^5.28.0", + "@sentry/tracing": "^5.28.0", + "@terra-money/terra.js": "^1.3.1", + "@testing-library/jest-dom": "^5.11.6", + "@testing-library/react": "^11.2.2", + "@testing-library/user-event": "^12.2.2", + "@tippyjs/react": "^4.2.0", + "@types/chart.js": "^2.9.28", + "@types/classnames": "^2.2.11", + "@types/jest": "^26.0.15", + "@types/node": "^14.14.10", + "@types/numeral": "^0.0.28", + "@types/ramda": "^0.27.32", + "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", + "@types/react-modal": "^3.10.6", + "@types/react-router-dom": "^5.1.6", + "bignumber.js": "^9.0.1", + "chart.js": "^2.9.4", + "classnames": "^2.2.6", + "date-fns": "^2.16.1", + "ethers": "^5.0.23", + "graphql": "^15.4.0", + "husky": "^4.3.0", + "lint-staged": "^10.5.2", + "node-sass": "^4", + "numeral": "^2.0.6", + "prettier": "^2.2.0", + "ramda": "^0.27.1", + "react": "^17.0.1", + "react-chartjs-2": "^2.11.1", + "react-dom": "^17.0.1", + "react-modal": "^3.12.1", + "react-router-dom": "^5.2.0", + "react-scripts": "4.0.1", + "source-map-explorer": "^2.5.0", + "typescript": "~4.1.2", + "use-onclickoutside": "^0.3.1" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject", + "analyze": "source-map-explorer 'build/static/js/*.js'" + }, + "eslintConfig": { + "extends": "react-app", + "rules": { + "import/no-anonymous-default-export": "off" + } + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [ + "prettier --no-semi --write" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "prettier": { + "semi": false + } +} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 000000000..e975dec99 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,10 @@ + + + + diff --git a/public/index.html b/public/index.html new file mode 100644 index 000000000..7d272fbb7 --- /dev/null +++ b/public/index.html @@ -0,0 +1,23 @@ + + + + + + + + + Mirror + + + + + + +
+ + diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/src/_customize.scss b/src/_customize.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/airdrop/Airdrop.svg b/src/airdrop/Airdrop.svg new file mode 100644 index 000000000..e25ff18e6 --- /dev/null +++ b/src/airdrop/Airdrop.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/airdrop/AirdropToast.module.scss b/src/airdrop/AirdropToast.module.scss new file mode 100644 index 000000000..bc44fdd45 --- /dev/null +++ b/src/airdrop/AirdropToast.module.scss @@ -0,0 +1,60 @@ +@import "mixins"; + +.toast { + position: fixed; + top: ($nav-height + $gutter); + right: $gutter; + z-index: 9999; + + background: $airdrop; + border: 1px solid fade-out($blue, 0.5); + border-radius: 10px; + box-shadow: 0 10px 40px 0 fade-out(black, 0.7); + padding: $gutter; + text-align: center; + width: 260px; +} + +.close { + position: absolute; + top: 20px; + right: 20px; +} + +.image { + animation: rotate 2s infinite; + animation-timing-function: linear; + transform-origin: bottom; +} + +@keyframes rotate { + 25% { + transform: rotate(-10deg); + } + + 50% { + transform: rotate(0); + } + + 75% { + transform: rotate(10deg); + } + + 100% { + transform: rotate(0); + } +} + +.header { + color: white; + font-size: 20px; + font-weight: 500; + margin-top: 10px; + margin-bottom: 2px; +} + +.content { + color: $gray; + font-size: 12px; + margin-bottom: $gutter; +} diff --git a/src/airdrop/AirdropToast.tsx b/src/airdrop/AirdropToast.tsx new file mode 100644 index 000000000..cce0248e1 --- /dev/null +++ b/src/airdrop/AirdropToast.tsx @@ -0,0 +1,28 @@ +import { useState } from "react" +import { ReactComponent as Image } from "./Airdrop.svg" +import LinkButton from "../components/LinkButton" +import Icon from "../components/Icon" +import styles from "./AirdropToast.module.scss" + +const AirdropToast = () => { + const [isOpen, setIsOpen] = useState(true) + const close = () => setIsOpen(false) + + return !isOpen ? null : ( +
+ + + +
MIR Airdrop
+

Claim your MIR tokens

+ + + Claim + +
+ ) +} + +export default AirdropToast diff --git a/src/airdrop/airdrop.d.ts b/src/airdrop/airdrop.d.ts new file mode 100644 index 000000000..01c9159bc --- /dev/null +++ b/src/airdrop/airdrop.d.ts @@ -0,0 +1,6 @@ +interface Airdrop { + stage: number + address: string + amount: string + proof: string +} diff --git a/src/airdrop/gqldocs.ts b/src/airdrop/gqldocs.ts new file mode 100644 index 000000000..8dd200372 --- /dev/null +++ b/src/airdrop/gqldocs.ts @@ -0,0 +1,9 @@ +import { gql } from "@apollo/client" + +const AIRDROP = gql` + query airdrop($address: String!, $network: String = "TERRA") { + airdrop(address: $address, network: $network) + } +` + +export default AIRDROP diff --git a/src/components/AppFooter.module.scss b/src/components/AppFooter.module.scss new file mode 100644 index 000000000..7ed1f8c7a --- /dev/null +++ b/src/components/AppFooter.module.scss @@ -0,0 +1,56 @@ +@import "mixins"; +@import "variables"; + +.footer { + color: $slate; + + @include desktop { + height: $footer-height; + } + + @include mobile { + height: $footer-height-mobile; + } + + position: absolute; + bottom: 0; + width: 100%; +} + +.container { + @include desktop { + @include flex(space-between); + } +} + +/* network */ +.network { + @include flex; + color: white; + text-transform: capitalize; + + i { + margin-right: 5px; + } +} + +/* author */ +.community { + @include flex; + margin: -10px; + + @include mobile { + margin-top: 20px; + } +} + +.link { + @include flex; + @include transition(opacity); + padding: 10px; + opacity: 0.3; + + &:hover { + opacity: 1; + } +} diff --git a/src/components/AppFooter.tsx b/src/components/AppFooter.tsx new file mode 100644 index 000000000..544d68a6e --- /dev/null +++ b/src/components/AppFooter.tsx @@ -0,0 +1,73 @@ +import medium from "./Community/medium.png" +import discord from "./Community/discord.png" +import telegram from "./Community/telegram.png" +import twitter from "./Community/twitter.png" +import github from "./Community/github.png" + +import Container from "./Container" +import ExtLink from "./ExtLink" +import Icon from "./Icon" +import styles from "./AppFooter.module.scss" + +interface Props { + network?: string + project: string +} + +const AppFooter = ({ network, project }: Props) => { + const community = [ + { + href: `https://github.com/Mirror-Protocol/${project}`, + src: github, + alt: "Github", + }, + { + href: "https://medium.com/@mirror-protocol", + src: medium, + alt: "Medium", + }, + { + href: "https://t.me/mirror_protocol", + src: telegram, + alt: "Telegram", + }, + { + href: "https://discord.gg/KYC22sngFn", + src: discord, + alt: "Discord", + }, + { + href: "https://twitter.com/mirror_protocol", + src: twitter, + alt: "Twitter", + }, + ] + + return ( + + ) +} + +export default AppFooter diff --git a/src/components/AppHeader.module.scss b/src/components/AppHeader.module.scss new file mode 100644 index 000000000..017cfc70b --- /dev/null +++ b/src/components/AppHeader.module.scss @@ -0,0 +1,69 @@ +@import "mixins"; +@import "variables"; + +.header { + background: $header; + box-shadow: 0 10px 10px 0 fade-out($bg, 0.5); + position: sticky; + top: 0; + z-index: $zindex-sticky; +} + +@include mobile { + .header { + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; + } + + .collapsed { + position: sticky; + } +} + +.container { + @include desktop { + @include flex(space-between); + } +} + +.wrapper { + @include flex(space-between); +} + +.logo { + @include flex; + height: $nav-height; +} + +.toggle { + @include desktop { + display: none; + } +} + +.support { + @include desktop { + @include flex; + } + + @include mobile { + .collapsed & { + display: none; + } + } +} + +.connect { + padding-left: 15px; + + @include mobile { + display: none; + } +} + +.hr { + margin: 0; +} diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx new file mode 100644 index 000000000..c1e5dee1d --- /dev/null +++ b/src/components/AppHeader.tsx @@ -0,0 +1,58 @@ +import { useState, useEffect, ReactNode } from "react" +import { Link, useLocation } from "react-router-dom" +import classNames from "classnames/bind" +import Container from "./Container" +import Icon from "./Icon" +import Menu from "./Menu" +import styles from "./AppHeader.module.scss" + +const cx = classNames.bind(styles) + +interface Props { + logo: ReactNode + menu: MenuItem[] + connect: ReactNode + border?: boolean +} + +const AppHeader = ({ logo, menu, connect, border }: Props) => { + const { key } = useLocation() + const [isOpen, setIsOpen] = useState(false) + const toggle = () => setIsOpen(!isOpen) + const hideToggle = menu.every((item) => item.desktopOnly) + + useEffect(() => { + setIsOpen(false) + }, [key]) + + return ( +
+ +
+
+

+ + {logo} + +

+ + {!hideToggle && ( + + )} +
+ +
+ +
{connect}
+
+
+ + {border &&
} +
+
+ ) +} + +export default AppHeader diff --git a/src/components/Badge.module.scss b/src/components/Badge.module.scss new file mode 100644 index 000000000..628508581 --- /dev/null +++ b/src/components/Badge.module.scss @@ -0,0 +1,14 @@ +@import "mixins"; + +.badge { + $height: 21px; + @include flex; + display: inline-flex; + height: $height; + border-radius: ($height / 2); + background: fade-out(white, 0.9); + color: white; + font-size: 10px; + font-weight: 500; + min-width: 90px; +} diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx new file mode 100644 index 000000000..cb590b55c --- /dev/null +++ b/src/components/Badge.tsx @@ -0,0 +1,9 @@ +import { FC } from "react" +import classNames from "classnames" +import styles from "./Badge.module.scss" + +const Badge: FC<{ className: string }> = ({ className, children }) => ( + {children} +) + +export default Badge diff --git a/src/components/Button.module.scss b/src/components/Button.module.scss new file mode 100644 index 000000000..c7b3ea6b8 --- /dev/null +++ b/src/components/Button.module.scss @@ -0,0 +1,115 @@ +@import "mixins"; +@import "variables"; + +.button { + @include flex; + @include button; + @include transition; + + display: inline-flex; + font-weight: 500; + user-select: none; + + &:hover { + text-decoration: none; + } + + & + & { + margin-left: 10px; + } + + // sequence important + &.disabled { + opacity: 0.3; + } + + &.loading { + opacity: 0.5; + } +} + +@mixin hover { + &:hover:not(.loading):not(.disabled) { + @content; + } +} + +.button:not(.outline) { + background-color: $blue; + color: $button; + + @include hover { + background-color: fade-out($blue, 0.25); + } + + &.disabled { + background-color: $slate; + } +} + +/* outline */ +.outline { + border-width: 1px; + border-style: solid; + + @include hover { + opacity: 0.75; + } + + @mixin button-outline-variant($color) { + border-color: $color; + color: $color; + } + + @each $name, $color in $colors { + &.#{$name} { + @include button-outline-variant($color); + } + } + + &.secondary { + @include button-outline-variant(fade-out(white, 0.4)); + } +} + +/* block */ +.block { + width: 100%; +} + +/* sizes */ +@mixin button-size($font-size, $height, $padding) { + border-radius: ($height / 2); + font-size: $font-size; + height: $height; + padding: 0 $padding; +} + +.xs { + @include button-size(10px, 22px, 10px); +} + +.sm { + @include button-size(12px, 26px, 10px); + min-width: 74px; +} + +.md { + @include button-size(14px, 36px, 20px); + min-width: 160px; +} + +.lg { + @include button-size(16px, 50px, 30px); + width: 100%; +} + +/* theme */ +.submit { + margin-top: 30px; +} + +/* label */ +.progress { + margin-right: 10px; +} diff --git a/src/components/Button.tsx b/src/components/Button.tsx new file mode 100644 index 000000000..d8f71998d --- /dev/null +++ b/src/components/Button.tsx @@ -0,0 +1,26 @@ +import classNames from "classnames/bind" +import Loading from "./Loading" +import styles from "./Button.module.scss" + +const cx = classNames.bind(styles) + +const Button = (props: Button) => { + const { loading, children } = props + return ( + + ) +} + +export default Button + +/* styles */ +export const getAttrs = (props: T) => { + const { size = "md", color = "blue", outline, block, ...rest } = props + const { loading, submit, ...attrs } = rest + const status = { outline, block, loading, disabled: attrs.disabled, submit } + const className = cx(styles.button, size, color, status, attrs.className) + return { ...attrs, className } +} diff --git a/src/components/Card.module.scss b/src/components/Card.module.scss new file mode 100644 index 000000000..c522ef791 --- /dev/null +++ b/src/components/Card.module.scss @@ -0,0 +1,65 @@ +@import "mixins"; +@import "variables"; + +.card { + background: $darkblue; + + border-radius: 10px; + position: relative; + overflow: hidden; + + &.lg { + border-radius: 20px; + } +} + +.shadow { + box-shadow: 0 0 40px 0 fade-out(black, 0.7); +} + +.link { + @include transition(border-color); + border-width: 1px; + border-style: solid; + border-color: transparent; + + &:hover { + border-color: $blue; + } +} + +/* main */ +.main { + padding: $card-padding-main; + height: 100%; + + .lg & { + padding: $gutter $card-padding-horizontal; + } + + .full & { + padding: unset; + } +} + +/* badges */ +.badges { + @include flex(flex-start); + position: absolute; + top: 0; + left: 0; +} + +.badge { + @include flex; + display: inline-flex; + font-size: 11px; + height: 20px; + padding: 0 15px; +} + +@each $name, $color in $colors { + .bg-#{$name} { + background: fade-out($color, 0.5); + } +} diff --git a/src/components/Card.tsx b/src/components/Card.tsx new file mode 100644 index 000000000..446337d62 --- /dev/null +++ b/src/components/Card.tsx @@ -0,0 +1,69 @@ +import { FC, ReactNode } from "react" +import { Link } from "react-router-dom" +import classNames from "classnames/bind" +import CardHeader from "./CardHeader" +import styles from "./Card.module.scss" + +const cx = classNames.bind(styles) + +export interface Props { + /** Icon above title */ + icon?: ReactNode + header?: ReactNode + title?: ReactNode + description?: ReactNode + + /** Card acts as a link */ + to?: string + /** Button to the left of the title */ + goBack?: () => void + /** Button to the right of the title */ + action?: ReactNode + /** Badges */ + badges?: Badge[] + + /** Card class */ + className?: string + /** More padding and more rounded corners */ + lg?: boolean + /** No padding */ + full?: boolean + /** Box shadow */ + shadow?: boolean + /** Show loading indicator to the right of title */ + loading?: boolean +} + +interface Badge { + label: string + color: string +} + +const Card: FC = (props) => { + const { children, to, badges, className, lg, full, shadow } = props + + const attrs = { + className: cx(styles.card, { lg, full, link: to, shadow }, className), + children: ( + <> + + + {badges && ( +
+ {badges.map(({ label, color }) => ( + + {label} + + ))} +
+ )} + +
{children}
+ + ), + } + + return to ? :
+} + +export default Card diff --git a/src/components/CardHeader.module.scss b/src/components/CardHeader.module.scss new file mode 100644 index 000000000..c08836dd9 --- /dev/null +++ b/src/components/CardHeader.module.scss @@ -0,0 +1,56 @@ +@import "mixins"; +@import "variables"; + +.header { + border-bottom: 1px solid $hr; + box-shadow: inset 0 -1px $hr-shadow; + padding: $card-padding-header; +} + +.title { + color: white; + font-size: 16px; + text-transform: capitalize; +} + +.description:not(:empty) { + margin-top: 5px; +} + +/* theme */ +.default { + @include flex(space-between); + + .wrapper { + flex: 1; + margin-right: 20px; + } +} + +.goback { + @include flex(flex-start); + padding: unset; + + .action { + @include flex; + @include link(inherit); + flex: none; + padding: $card-padding-header; + } + + .title { + flex: 1; + text-align: center; + padding-right: (24px + (2 * $card-padding-horizontal)); + } +} + +.icon { + text-align: center; + + .wrapper { + @include flex; + min-height: 50px; + margin-bottom: 5px; + } +} diff --git a/src/components/CardHeader.tsx b/src/components/CardHeader.tsx new file mode 100644 index 000000000..6224a8adb --- /dev/null +++ b/src/components/CardHeader.tsx @@ -0,0 +1,76 @@ +import { FC } from "react" +import classNames from "classnames/bind" +import Icon from "./Icon" +import LoadingTitle from "./LoadingTitle" +import { Props } from "./Card" +import styles from "./CardHeader.module.scss" + +enum HeaderType { + /** (align:left) title + description + loading + action */ + DEFAULT, + /** (align:center) goBack */ + GOBACK, + /** (align:center) */ + ICON, +} + +const CardHeader: FC = ({ header, title, ...props }) => { + const { icon, description, goBack, action, loading } = props + + const headerType = icon + ? HeaderType.ICON + : goBack + ? HeaderType.GOBACK + : HeaderType.DEFAULT + + const className = { + [HeaderType.DEFAULT]: styles.default, + [HeaderType.GOBACK]: styles.goback, + [HeaderType.ICON]: styles.icon, + }[headerType] + + const render = { + [HeaderType.DEFAULT]: ( + <> +
+ +

{title}

+
+ + {description && ( +
{description}
+ )} +
+ + {action &&
{action}
} + + ), + + [HeaderType.GOBACK]: ( + <> + {goBack && ( + + )} + +

{title}

+ + ), + + [HeaderType.ICON]: ( + <> +
{icon}
+

{title}

+ + ), + }[headerType] + + return !(header || title) ? null : ( +
+ {header ?? render} +
+ ) +} + +export default CardHeader diff --git a/src/components/Change.module.scss b/src/components/Change.module.scss new file mode 100644 index 000000000..5ee55faad --- /dev/null +++ b/src/components/Change.module.scss @@ -0,0 +1,26 @@ +@import "mixins"; +@import "variables"; + +.flex { + @include flex(flex-start); +} + +.change { + font-size: 12px; + + &:not(:first-child) { + margin-left: 10px; + } + + i { + margin-right: 5px; + } +} + +.up { + color: $aqua; +} + +.down { + color: $red; +} diff --git a/src/components/Change.tsx b/src/components/Change.tsx new file mode 100644 index 000000000..d57891ce9 --- /dev/null +++ b/src/components/Change.tsx @@ -0,0 +1,41 @@ +import { ReactNode } from "react" +import classNames from "classnames/bind" +import { abs, gt, gte, lt } from "../libs/math" +import { percent } from "../libs/num" +import Icon from "./Icon" +import styles from "./Change.module.scss" + +const cx = classNames.bind(styles) + +interface Props { + price?: ReactNode + className?: string + children?: string +} + +const Change = ({ price, className, children }: Props) => { + const change = children && (gte(abs(children), 0.0001) ? children : "0") + + const render = (change: string) => { + const up = gt(change, 0) + const down = lt(change, 0) + const icon = up ? "trending_up" : down ? "trending_down" : "arrow_right_alt" + return ( + + + {percent(abs(change))} + + ) + } + + return !(price || change) ? null : change ? ( + render(change) + ) : ( + + {price} + {change && render(change)} + + ) +} + +export default Change diff --git a/src/components/Checkbox.module.scss b/src/components/Checkbox.module.scss new file mode 100644 index 000000000..04f5dd778 --- /dev/null +++ b/src/components/Checkbox.module.scss @@ -0,0 +1,40 @@ +@import "mixins"; +@import "variables"; + +.label { + @include flex(flex-start); + cursor: pointer; + user-select: none; +} + +.input { + @include flex; + border: solid 1px $slate; + border-radius: 3px; + margin-right: 5px; + width: 18px; + height: 18px; +} + +.check { + @include transition(background-color); + border-radius: 1px; + width: 10px; + height: 10px; + + .label:hover &:not(.checked) { + background-color: fade-out($blue, 0.5); + } +} + +.checked { + background-color: $blue; +} + +/* radio */ +.radio { + &, + .check { + border-radius: 50%; + } +} diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx new file mode 100644 index 000000000..a73b15d0f --- /dev/null +++ b/src/components/Checkbox.tsx @@ -0,0 +1,26 @@ +import { FC, LabelHTMLAttributes } from "react" +import classNames from "classnames/bind" +import styles from "./Checkbox.module.scss" + +const cx = classNames.bind(styles) + +interface Props extends LabelHTMLAttributes { + type?: "checkbox" | "radio" + checked: boolean + className?: string +} + +const Checkbox: FC = (props) => { + const { type = "checkbox", checked, className, children, ...attrs } = props + + return ( +