diff --git a/.babelrc b/.babelrc index 48cec0a..1ff94f7 100644 --- a/.babelrc +++ b/.babelrc @@ -1,14 +1,3 @@ { - "presets": [ - "next/babel" - ], - "plugins": [ - [ - "import", - { - "libraryName": "antd", - "style": true - } - ] - ] + "presets": ["next/babel"] } diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..35e915e --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +**/node_modules/* +**/out/* +**/.next/* diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..6dd5286 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,47 @@ +{ + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:@typescript-eslint/recommended" + // Uncomment the following lines to enable eslint-config-prettier + // Is not enabled right now to avoid issues with the Next.js repo + // "prettier", + ], + "env": { + "es6": true, + "browser": true, + "jest": true, + "node": true + }, + "settings": { + "react": { + "version": "detect" + } + }, + "rules": { + "react/react-in-jsx-scope": 0, + "react/display-name": 0, + "react/prop-types": 0, + "@typescript-eslint/explicit-function-return-type": 0, + "@typescript-eslint/explicit-member-accessibility": 0, + "@typescript-eslint/indent": 0, + "@typescript-eslint/member-delimiter-style": 0, + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-var-requires": 0, + "@typescript-eslint/no-use-before-define": 0, + "@typescript-eslint/no-unused-vars": [ + 2, + { + "argsIgnorePattern": "^_" + } + ], + "no-console": [ + 2, + { + "allow": ["warn", "error"] + } + ] + } +} diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000..31354ec --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..d2ae35e --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn lint-staged diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..e1e6209 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +yarn type-check && yarn lint diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..c9b91a4 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +node_modules +.next +yarn.lock +package-lock.json +public +components/charting_library diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..b2095be --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "semi": false, + "singleQuote": true +} diff --git a/@types/index.d.ts b/@types/index.d.ts new file mode 100644 index 0000000..9b9471d --- /dev/null +++ b/@types/index.d.ts @@ -0,0 +1,4 @@ +declare module '*.svg' { + const content: any + export default content +} diff --git a/@types/types.tsx b/@types/types.tsx new file mode 100644 index 0000000..04f0885 --- /dev/null +++ b/@types/types.tsx @@ -0,0 +1,62 @@ +import { + AccountInfo, + Connection, + PublicKey, + Transaction, +} from '@solana/web3.js' +import Wallet from '@project-serum/sol-wallet-adapter' + +export interface ConnectionContextValues { + endpoint: string + setEndpoint: (newEndpoint: string) => void + connection: Connection + sendConnection: Connection + availableEndpoints: EndpointInfo[] + setCustomEndpoints: (newCustomEndpoints: EndpointInfo[]) => void +} + +export interface EndpointInfo { + name: string + url: string + websocket: string +} + +export interface WalletContextValues { + wallet: Wallet + connected: boolean + providerUrl: string + setProviderUrl: (newProviderUrl: string) => void + providerName: string +} + +export interface TokenAccount { + pubkey: PublicKey + account: AccountInfo | null + effectiveMint: PublicKey +} + +/** + * {tokenMint: preferred token account's base58 encoded public key} + */ +export interface SelectedTokenAccounts { + [tokenMint: string]: string +} + +// Token infos +export interface KnownToken { + tokenSymbol: string + tokenName: string + icon?: string + mintAddress: string +} + +export interface WalletAdapter { + publicKey: PublicKey + autoApprove: boolean + connected: boolean + signTransaction: (transaction: Transaction) => Promise + signAllTransactions: (transaction: Transaction[]) => Promise + connect: () => any + disconnect: () => any + on(event: string, fn: () => void): this +} diff --git a/README.md b/README.md index 514bf1d..9f84531 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,27 @@ -# TypeScript Next.js example +# NextJS Typescript Boilerplate -This is a really simple project that shows the usage of Next.js with TypeScript. +Bootstrap a developer-friendly NextJS app configured with: + +- [Typescript](https://www.typescriptlang.org/) +- Linting with [ESLint](https://eslint.org/) +- Formatting with [Prettier](https://prettier.io/) +- Linting, typechecking and formatting on by default using [`husky`](https://github.com/typicode/husky) for commit hooks +- Testing with [Jest](https://jestjs.io/) and [`react-testing-library`](https://testing-library.com/docs/react-testing-library/intro) ## Deploy your own Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example): -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-typescript&project-name=with-typescript&repository-name=with-typescript) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-typescript-eslint-jest&project-name=with-typescript-eslint-jest&repository-name=with-typescript-eslint-jest) -## How to use it? +## How to use Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example: ```bash -npx create-next-app --example with-typescript with-typescript-app +npx create-next-app --example with-typescript-eslint-jest with-typescript-eslint-jest-app # or -yarn create next-app --example with-typescript with-typescript-app +yarn create next-app --example with-typescript-eslint-jest with-typescript-eslint-jest-app ``` Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). - -## Notes - -This example shows how to integrate the TypeScript type system into Next.js. Since TypeScript is supported out of the box with Next.js, all we have to do is to install TypeScript. - -``` -npm install --save-dev typescript -``` - -To enable TypeScript's features, we install the type declarations for React and Node. - -``` -npm install --save-dev @types/react @types/react-dom @types/node -``` - -When we run `next dev` the next time, Next.js will start looking for any `.ts` or `.tsx` files in our project and builds it. It even automatically creates a `tsconfig.json` file for our project with the recommended settings. - -Next.js has built-in TypeScript declarations, so we'll get autocompletion for Next.js' modules straight away. - -A `type-check` script is also added to `package.json`, which runs TypeScript's `tsc` CLI in `noEmit` mode to run type-checking separately. You can then include this, for example, in your `test` scripts. diff --git a/components/Balances.tsx b/components/Balances.tsx new file mode 100644 index 0000000..daea26f --- /dev/null +++ b/components/Balances.tsx @@ -0,0 +1,35 @@ +import BN from 'bn.js' + +import useWalletStore from '../stores/useWalletStore' + +const Balances = () => { + const { tokenAccounts, mints } = useWalletStore((state) => state) + + function fixedPointToNumber(value: BN, decimals: number) { + const divisor = new BN(10).pow(new BN(decimals)) + const quotient = value.div(divisor) + const remainder = value.mod(divisor) + return quotient.toNumber() + remainder.toNumber() / divisor.toNumber() + } + + function calculateBalance(a) { + const mint = mints[a.account.mint.toBase58()] + return mint ? fixedPointToNumber(a.account.amount, mint.decimals) : 0 + } + + const displayedBalances = tokenAccounts + .map((a) => ({ id: a.publicKey.toBase58(), balance: calculateBalance(a) })) + .sort((a, b) => (a.id > b.id ? 1 : -1)) + + return ( +
    + {displayedBalances.map((b) => ( +
  • + {b.id}: {b.balance} +
  • + ))} +
+ ) +} + +export default Balances diff --git a/components/CardSection.tsx b/components/CardSection.tsx new file mode 100644 index 0000000..7ab2568 --- /dev/null +++ b/components/CardSection.tsx @@ -0,0 +1,24 @@ +const CardSection = () => { + return ( +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ ) +} + +export default CardSection diff --git a/components/ConnectWalletButton.tsx b/components/ConnectWalletButton.tsx new file mode 100644 index 0000000..e293549 --- /dev/null +++ b/components/ConnectWalletButton.tsx @@ -0,0 +1,45 @@ +import styled from '@emotion/styled' +import useWalletStore from '../stores/useWalletStore' +import { WALLET_PROVIDERS, DEFAULT_PROVIDER } from '../hooks/useWallet' +import useLocalStorageState from '../hooks/useLocalStorageState' +import WalletSelect from './WalletSelect' +import WalletIcon from './WalletIcon' + +const StyledWalletTypeLabel = styled.div` + font-size: 0.6rem; +` + +const ConnectWalletButton = () => { + const wallet = useWalletStore((s) => s.current) + const [savedProviderUrl] = useLocalStorageState( + 'walletProvider', + DEFAULT_PROVIDER.url + ) + + return ( +
+ +
+ +
+
+ ) +} + +export default ConnectWalletButton diff --git a/components/ContactIcons.tsx b/components/ContactIcons.tsx deleted file mode 100644 index b097f42..0000000 --- a/components/ContactIcons.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Space } from "antd"; - -export default function ContactIcons() { - return ( - <> - - - - - - - - - - - - - - - - - - - ); -} diff --git a/components/EmailSection.tsx b/components/EmailSection.tsx new file mode 100644 index 0000000..fb2bee7 --- /dev/null +++ b/components/EmailSection.tsx @@ -0,0 +1,36 @@ +const EmailSection = () => { + return ( +
+
+
+
+

+ Keep in touch through email. +

+
+
+
+ +
+
+ +
+
+

We promise to never spam and only send alpha.

+
+
+
+
+
+
+
+ ) +} + +export default EmailSection diff --git a/components/FeatureSection.tsx b/components/FeatureSection.tsx new file mode 100644 index 0000000..9a86d48 --- /dev/null +++ b/components/FeatureSection.tsx @@ -0,0 +1,350 @@ +const FeatureSection = () => { + return ( +
+
+
+
+
+

+ Simple, intuitive, and fast.{' '} +

+

+ The Mango margin protocol is a fully open-source margin trading + exchange. Its best in class user interface provides access to + deep liquidity and high leverage for traders, built by traders.{' '} +

+
+ +
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +

+ Maximum capital efficiency +

+

+ Utilize all your assets as collateral to trade any other asset + with up to 5x leverage.{' '} +

+
+ +
+ + + + + + + + + + + +

+ Sub-second latency +

+

+ Low latency allows market makers to post tight spreads on the + books. That means Instant order execution at the price of less + than a cent. +

+
+
+ + + + + + + + + + + +

+ The lowest fees +

+

+ Trade with the lowest fee possible. SRM deposits are pooled for + a collective discount. Mango is the first protocol to charge + zero fees on margin borrowing & lending. +

+
+
+ +
+
+ + + + + + + + + + + +

+ Fully decentralized +

+

+ No more centralized counter party risk to deal with. Non + custodial means you control your funds. Every bid & ask is + on-chain. +

+
+
+ + + + + + + + + + + +

+ Want to help build Mango? +

+

+ Mango is a fully open-source project built by a global team of + contributors. Help build the world’s best exchange, period. +

+ + Join the discord » + +
+
+
+ +
+
+
+
+
+
+

+ Simply connect your wallet. +

+

+ Trade knowing you control your funds, no more centralized + counter-party risk. +

+
+
+
+
+
+
+
+
+

+ Customize your experience. +

+

+ Complete control over layout, theme, and your trading + view. +

+
+
+
+
+
+ +
+
+
+
+
+

+ Measure your results. +

+

+ Full overview over trading history and PNL is always + tracked. +

+
+
+
+
+
+
+
+
+

+ Liquidation alerts +

+

+ Sleep knowing you’ll be ready when the market starts + moving.{' '} +

+
+
+
+
+
+
+
+
+ ) +} + +export default FeatureSection diff --git a/components/FloatingElement.jsx b/components/FloatingElement.jsx deleted file mode 100644 index d0b36a1..0000000 --- a/components/FloatingElement.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; - -const Wrapper = styled.div` - margin: 5px; - padding: 20px; - background-color: #141026; - border: 1px solid #584f81; - border-radius: 9px; -`; - -export default function FloatingElement({ children, style = undefined, stretchVertical = false }) { - return ( - - {children} - - ); -} - diff --git a/components/FooterSection.tsx b/components/FooterSection.tsx new file mode 100644 index 0000000..8f57dac --- /dev/null +++ b/components/FooterSection.tsx @@ -0,0 +1,142 @@ +import MangoPill from '../components/MangoPill' + +const FooterSection = () => { + return ( +
+
+
+
+
+

+ Keep in touch through email. +

+
+
+
+ +
+
+ +
+
+

We promise to never spam and only send alpha.

+
+
+
+
+
+
+ +
+
+ ) +} + +export default FooterSection diff --git a/components/HeroSection.tsx b/components/HeroSection.tsx new file mode 100644 index 0000000..855a1f1 --- /dev/null +++ b/components/HeroSection.tsx @@ -0,0 +1,19 @@ +const HeroSection = () => { + return ( +
+
+
+
+
+

+ Decentralized margin trading that feels right. +

+
+
+
+
+
+ ) +} + +export default HeroSection diff --git a/components/HeroSectionLP.tsx b/components/HeroSectionLP.tsx new file mode 100644 index 0000000..fa4872d --- /dev/null +++ b/components/HeroSectionLP.tsx @@ -0,0 +1,32 @@ +const HeroSectionLP = () => { + return ( +
+
+
+
+
+

+ Join the{' '} + + Mango DAO + {' '} + and help build the ecosystem. +

+

+ The Mango DAO is an experiment in self governance that aims to + build a completely decentralzied financial ecosystem. +

+
+ {/* +
+ +
+ */} +
+
+
+
+ ) +} + +export default HeroSectionLP diff --git a/components/LandingContent.tsx b/components/LandingContent.tsx new file mode 100644 index 0000000..9be0264 --- /dev/null +++ b/components/LandingContent.tsx @@ -0,0 +1,184 @@ +const LandingContent = () => { + return ( +
+
+
+

+ It is still the early days. +

+

+ This is the first moment for non-developers to participate in + helping build the Mango protocol by supporting the inception of the + protocols Insurance Fund. +

+
+ + {/* Section 1 */} +
+
+

+ What is Mango? +

+

+ Mango is a decentralized autonomous organization. Its purpose is + to contribute maximum value for the defi ecosystem and its + developer community to create commercially viable decentralized + trading and lending products for traders. +

+ +

+ Why the{' '} + + Insurance fund + + ? +

+

+ Mango protocol is powered by lenders providing their capital for + the community to use for trading and borrowing purposes. The + insurance fund is the last line of defense for protecting our + mango lenders. +

+
+ +
+

+ What is the{' '} + + $MNGO + {' '} + token? +

+

+ We believe that substantial rewards to a strong developer + community and liquidity incentives are the essential drivers for + growth and therefore the foundation of the Mango DAO. +

+

+ Mango Governance tokens ($MNGO) will serve as the incentive for + those who can proove their work is useful to the DAO. +

+ +

+ $MNGO were only provided to + developers who helped to build out the protocol. +

+
+
+ + {/* Section 2 */} +
+

+ How it works. +

+

+ We take the view that token sales should be simple, transparent and + minimize randomness and luck in the distribution. +

+
+
+
+
+
+
+
+

+ Deposit your USDC contribution. +

+

+ Users deposit USDC into a vault during the event period to + set their contribution amount. +

+
+
+
+
+
+
+
+
+

+ 48 hour participation period. +

+

+ The event will span over a 2 day period split into two + sections,{' '} + Unrestricted and{' '} + Restricted. +

+
+
+
+
+
+ +
+
+
+
+
+

+ Why does it work this way? +

+

+ Simple mechanisms are easier to build, explain, understand + and are harder to exploit. A transparent mechanism + increases participation because buyers are more confident + there are no hidden tricks that could harm them. +

+

+ Elements of luck engineered into the mechanism distribute + value randomly to those, who are most willing to do the + arbitrary, worthless tasks to get the free value. +

+

+ We believe all "excess" value should be captured by token + holders in the DAO. +

+
+
+
+
+
+
+
+
+

+ MNGO unlocked and distributed. +

+

+ At event conclusion $MNGO gets distributed in propotion to + a users USDC contribution.{' '} +

+
+
+
+
+
+
+
+
+ ) +} + +export default LandingContent diff --git a/components/Logo.tsx b/components/Logo.tsx deleted file mode 100644 index 724600c..0000000 --- a/components/Logo.tsx +++ /dev/null @@ -1,22 +0,0 @@ -export default function Logo() { - return ( - <> - -
- Mango Markets -
- - ); -} diff --git a/components/MangoPill.tsx b/components/MangoPill.tsx new file mode 100644 index 0000000..d4cb647 --- /dev/null +++ b/components/MangoPill.tsx @@ -0,0 +1,9 @@ +const MangoPill = () => { + return ( +
+

Soon

+
+ ) +} + +export default MangoPill diff --git a/components/MangoSale.tsx b/components/MangoSale.tsx new file mode 100644 index 0000000..7d3a1b9 --- /dev/null +++ b/components/MangoSale.tsx @@ -0,0 +1,9 @@ +const MangoSale = () => { + return ( +
+

Sale

+
+ ) +} + +export default MangoSale diff --git a/components/NavBarBeta.tsx b/components/NavBarBeta.tsx new file mode 100644 index 0000000..cbede50 --- /dev/null +++ b/components/NavBarBeta.tsx @@ -0,0 +1,554 @@ +import MangoPill from '../components/MangoPill' +import MangoSale from '../components/MangoSale' + +const NavBarBeta = () => { + return ( +
+ {/* Main Menu */} + +
+ ) +} + +export default NavBarBeta diff --git a/components/Navigation.tsx b/components/Navigation.tsx deleted file mode 100644 index d449934..0000000 --- a/components/Navigation.tsx +++ /dev/null @@ -1,34 +0,0 @@ -export const TradeUrl = "https://trade.mango.markets"; -export const StatsUrl = `${TradeUrl}/stats`; -export const LearnUrl = "https://docs.mango.markets"; - -export function Navigation() { - return ( -
- -
- - - Mango Markets - -
-
- - - Trade - - - - Stats - - - - Learn - - - - Careers - -
- ); -} diff --git a/components/Notification.tsx b/components/Notification.tsx new file mode 100644 index 0000000..9c2c251 --- /dev/null +++ b/components/Notification.tsx @@ -0,0 +1,113 @@ +import { useEffect, useState } from 'react' +import { + CheckCircleIcon, + InformationCircleIcon, + XCircleIcon, +} from '@heroicons/react/outline' +import useNotificationStore from '../stores/useNotificationStore' + +const NotificationList = () => { + const { notifications, set: setNotificationStore } = useNotificationStore( + (s) => s + ) + + useEffect(() => { + if (notifications.length > 0) { + const id = setInterval(() => { + setNotificationStore((state) => { + state.notifications = notifications.slice(1, notifications.length) + }) + }, 5000) + + return () => { + clearInterval(id) + } + } + }, [notifications, setNotificationStore]) + + const reversedNotifications = [...notifications].reverse() + + return ( +
+
+ {reversedNotifications.map((n, idx) => ( + + ))} +
+
+ ) +} + +const Notification = ({ type, message, description, txid }) => { + const [showNotification, setShowNotification] = useState(true) + + if (!showNotification) return null + + return ( +
+
+
+
+ {type === 'success' ? ( + + ) : null} + {type === 'info' && ( + + )} + {type === 'error' && ( + + )} +
+
+
{message}
+ {description ? ( +

{description}

+ ) : null} + {txid ? ( + + View transaction {txid.slice(0, 8)}... + {txid.slice(txid.length - 8)} + + ) : null} +
+
+ +
+
+
+
+ ) +} + +export default NotificationList diff --git a/components/StatsPanel.jsx b/components/StatsPanel.jsx deleted file mode 100644 index d233dd1..0000000 --- a/components/StatsPanel.jsx +++ /dev/null @@ -1,128 +0,0 @@ -import { Col, Row, Divider } from "antd"; -import { useEffect, useState } from "react"; -import styled from "styled-components"; - -import { IDS, MangoClient } from "@blockworks-foundation/mango-client"; -import { PublicKey, Connection } from "@solana/web3.js"; - -import FloatingElement from "./FloatingElement"; - -const CLUSTER = "mainnet-beta"; -const DEFAULT_MANGO_GROUP = "BTC_ETH_SOL_SRM_USDC"; - -const icons = { - BTC: "/tokens/btc.svg", - ETH: "/tokens/eth.svg", - SOL: "/tokens/sol.svg", - SRM: "/tokens/srm.svg", - USDC: "/tokens/usdc.svg", - USDT: "/tokens/usdt.svg", -}; - -const decimals = { - BTC: 2, - ETH: 2, - SOL: 1, - SRM: 0, - USDC: 0, -} - -const stubStats = { - depositInterest: 0, - borrowInterest: 0, - totalDeposits: 0, - totalBorrows: 0, - utilization: "0", -}; - -const useMangoStats = () => { - const [stats, setStats] = useState(Object.keys(icons).map((s) => ({ symbol: s, ...stubStats }))); - - useEffect(() => { - const getStats = async () => { - const client = new MangoClient(); - const connection = new Connection(IDS.cluster_urls[CLUSTER], "singleGossip"); - const assets = IDS[CLUSTER].mango_groups?.[DEFAULT_MANGO_GROUP]?.symbols; - const mangoGroupId = IDS[CLUSTER].mango_groups?.[DEFAULT_MANGO_GROUP]?.mango_group_pk; - if (!mangoGroupId) return; - const mangoGroupPk = new PublicKey(mangoGroupId); - const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk); - const latestStats = Object.keys(assets).map((symbol, index) => { - const totalDeposits = mangoGroup.getUiTotalDeposit(index); - const totalBorrows = mangoGroup.getUiTotalBorrow(index); - console.log("assets", symbol, index, totalDeposits, totalBorrows); - - return { - time: new Date(), - symbol, - totalDeposits, - totalBorrows, - depositInterest: mangoGroup.getDepositRate(index), - borrowInterest: mangoGroup.getBorrowRate(index), - utilization: totalDeposits > 0.0 ? totalBorrows / totalDeposits : 0.0, - }; - }); - setStats(latestStats); - }; - - getStats(); - }, []); - - return { stats }; -}; - -const Wrapper = styled.div` - height: 100%; - display: flex; - flex-direction: column; - padding: 16px 16px; - .borderNone .ant-select-selector { - border: none !important; - } -`; - -const SizeTitle = styled(Row)` - color: #9490a6; -`; - -export default function StatsPanel() { - const { stats } = useMangoStats(); - - return ( - - - - - - Mango Stats - - Asset - Total Deposits - Total Borrows - Deposit Interest - Borrow Interest - Utilization - - {stats.map((stat) => ( -
- - - - {stat.symbol} - - {stat.symbol} - {stat.totalDeposits.toFixed(decimals[stat.symbol])} - {stat.totalBorrows.toFixed(decimals[stat.symbol])} - {(100 * stat.depositInterest).toFixed(2)}% - {(100 * stat.borrowInterest).toFixed(2)}% - {(parseFloat(stat.utilization) * 100).toFixed(2)}% - -
- ))} -
-
- -
-
- ); -} diff --git a/components/TopBar.tsx b/components/TopBar.tsx new file mode 100644 index 0000000..c2eaf7b --- /dev/null +++ b/components/TopBar.tsx @@ -0,0 +1,116 @@ +import { useEffect, useState } from 'react' +import { Menu } from '@headlessui/react' +import styled from '@emotion/styled' +import { + ChevronUpIcon, + ChevronDownIcon, + DuplicateIcon, + LogoutIcon, +} from '@heroicons/react/outline' +import ConnectWalletButton from './ConnectWalletButton' +import WalletIcon from './WalletIcon' +import useWalletStore from '../stores/useWalletStore' + +const Code = styled.code` + border: 1px solid hsla(0, 0%, 39.2%, 0.2); + border-radius: 3px; + background: hsla(0, 0%, 58.8%, 0.1); + font-size: 13px; +` + +const WALLET_OPTIONS = [ + { name: 'Copy address', icon: }, + { name: 'Disconnect', icon: }, +] + +const TopBar = () => { + const { connected, current: wallet } = useWalletStore((s) => s) + const [isCopied, setIsCopied] = useState(false) + + useEffect(() => { + if (isCopied) { + const timer = setTimeout(() => { + setIsCopied(false) + }, 2000) + return () => clearTimeout(timer) + } + }, [isCopied]) + + const handleWalletMenu = (option) => { + if (option === 'Copy address') { + const el = document.createElement('textarea') + el.value = wallet.publicKey.toString() + document.body.appendChild(el) + el.select() + document.execCommand('copy') + document.body.removeChild(el) + setIsCopied(true) + } else { + wallet.disconnect() + } + } + + return ( + + ) +} + +export default TopBar diff --git a/components/WalletIcon.jsx b/components/WalletIcon.jsx new file mode 100644 index 0000000..9b03f05 --- /dev/null +++ b/components/WalletIcon.jsx @@ -0,0 +1,25 @@ +const WalletIcon = ({ className }) => { + return ( + + + + + ) +} + +export default WalletIcon diff --git a/components/WalletSelect.tsx b/components/WalletSelect.tsx new file mode 100644 index 0000000..4e7b77f --- /dev/null +++ b/components/WalletSelect.tsx @@ -0,0 +1,64 @@ +import { Menu } from '@headlessui/react' +import { + ChevronDownIcon, + ChevronUpIcon, + CheckCircleIcon, +} from '@heroicons/react/outline' + +import useWalletStore from '../stores/useWalletStore' +import { WALLET_PROVIDERS, DEFAULT_PROVIDER } from '../hooks/useWallet' +import useLocalStorageState from '../hooks/useLocalStorageState' + +export default function WalletSelect({ isPrimary = false }) { + const setWalletStore = useWalletStore((s) => s.set) + const [savedProviderUrl] = useLocalStorageState( + 'walletProvider', + DEFAULT_PROVIDER.url + ) + + const handleSelectProvider = (url) => { + setWalletStore((state) => { + state.providerUrl = url + }) + } + + return ( + + {({ open }) => ( + <> + + {open ? ( + + ) : ( + + )} + + + {WALLET_PROVIDERS.map(({ name, url, icon }) => ( + + + + ))} + + + )} + + ) +} diff --git a/hooks/useInterval.tsx b/hooks/useInterval.tsx new file mode 100644 index 0000000..df8a1a1 --- /dev/null +++ b/hooks/useInterval.tsx @@ -0,0 +1,39 @@ +import { useState, useRef, useEffect } from 'react' + +export function useEffectAfterTimeout(effect, timeout) { + useEffect(() => { + const handle = setTimeout(effect, timeout) + return () => clearTimeout(handle) + }) +} + +export function useListener(emitter, eventName) { + const [, forceUpdate] = useState(0) + useEffect(() => { + const listener = () => forceUpdate((i) => i + 1) + emitter.on(eventName, listener) + return () => emitter.removeListener(eventName, listener) + }, [emitter, eventName]) +} + +export default function useInterval(callback, delay) { + const savedCallback = useRef<() => void>() + + // Remember the latest callback. + useEffect(() => { + savedCallback.current = callback + }, [callback]) + + // Set up the interval. + useEffect(() => { + function tick() { + savedCallback.current && savedCallback.current() + } + if (delay !== null) { + const id = setInterval(tick, delay) + return () => { + clearInterval(id) + } + } + }, [delay]) +} diff --git a/hooks/useLocalStorageState.tsx b/hooks/useLocalStorageState.tsx new file mode 100644 index 0000000..ba65a63 --- /dev/null +++ b/hooks/useLocalStorageState.tsx @@ -0,0 +1,69 @@ +import { useMemo, useState, useEffect, useCallback } from 'react' + +const localStorageListeners = {} + +export function useLocalStorageStringState( + key: string, + defaultState: string | null = null +): [string | null, (newState: string | null) => void] { + const state = + typeof window !== 'undefined' + ? localStorage.getItem(key) || defaultState + : defaultState || '' + + const [, notify] = useState(key + '\n' + state) + + useEffect(() => { + if (!localStorageListeners[key]) { + localStorageListeners[key] = [] + } + localStorageListeners[key].push(notify) + return () => { + localStorageListeners[key] = localStorageListeners[key].filter( + (listener) => listener !== notify + ) + if (localStorageListeners[key].length === 0) { + delete localStorageListeners[key] + } + } + }, [key]) + + const setState = useCallback<(newState: string | null) => void>( + (newState) => { + if (!localStorageListeners[key]) { + localStorageListeners[key] = [] + } + const changed = state !== newState + if (!changed) { + return + } + + if (newState === null) { + localStorage.removeItem(key) + } else { + localStorage.setItem(key, newState) + } + localStorageListeners[key].forEach((listener) => + listener(key + '\n' + newState) + ) + }, + [state, key] + ) + + return [state, setState] +} + +export default function useLocalStorageState( + key: string, + defaultState: T | null = null +): [T, (newState: T) => void] { + const [stringState, setStringState] = useLocalStorageStringState( + key, + JSON.stringify(defaultState) + ) + + return [ + useMemo(() => stringState && JSON.parse(stringState), [stringState]), + (newState) => setStringState(JSON.stringify(newState)), + ] +} diff --git a/hooks/useWallet.tsx b/hooks/useWallet.tsx new file mode 100644 index 0000000..6948ffb --- /dev/null +++ b/hooks/useWallet.tsx @@ -0,0 +1,135 @@ +import { useEffect, useMemo } from 'react' +import Wallet from '@project-serum/sol-wallet-adapter' + +import { WalletAdapter } from '../@types/types' +import useWalletStore from '../stores/useWalletStore' +import { notify } from '../utils/notifications' +import { + PhantomWalletAdapter, + SolletExtensionAdapter, +} from '../utils/wallet-adapters' +import useInterval from './useInterval' +import useLocalStorageState from './useLocalStorageState' + +const SECONDS = 1000 +const ASSET_URL = + 'https://cdn.jsdelivr.net/gh/solana-labs/oyster@main/assets/wallets' + +export const WALLET_PROVIDERS = [ + { + name: 'Sollet.io', + url: 'https://www.sollet.io', + icon: `${ASSET_URL}/sollet.svg`, + }, + { + name: 'Sollet Extension', + url: 'https://www.sollet.io/extension', + icon: `${ASSET_URL}/sollet.svg`, + adapter: SolletExtensionAdapter as any, + }, + { + name: 'Phantom', + url: 'https://www.phantom.app', + icon: `https://www.phantom.app/img/logo.png`, + adapter: PhantomWalletAdapter, + }, +] + +export const DEFAULT_PROVIDER = WALLET_PROVIDERS[0] + +export default function useWallet() { + const { + connected, + connection: { endpoint }, + current: wallet, + providerUrl: selectedProviderUrl, + set: setWalletStore, + actions, + } = useWalletStore((state) => state) + const [savedProviderUrl, setSavedProviderUrl] = useLocalStorageState( + 'walletProvider', + DEFAULT_PROVIDER.url + ) + const provider = useMemo( + () => WALLET_PROVIDERS.find(({ url }) => url === savedProviderUrl), + [savedProviderUrl] + ) + + useEffect(() => { + if (selectedProviderUrl) { + setSavedProviderUrl(selectedProviderUrl) + } + }, [selectedProviderUrl]) + + useEffect(() => { + if (provider) { + const updateWallet = () => { + // hack to also update wallet synchronously in case it disconnects + const wallet = new (provider.adapter || Wallet)( + savedProviderUrl, + endpoint + ) as WalletAdapter + setWalletStore((state) => { + state.current = wallet + }) + } + + if (document.readyState !== 'complete') { + // wait to ensure that browser extensions are loaded + const listener = () => { + updateWallet() + window.removeEventListener('load', listener) + } + window.addEventListener('load', listener) + return () => window.removeEventListener('load', listener) + } else { + updateWallet() + } + } + }, [provider, savedProviderUrl, endpoint]) + + useEffect(() => { + if (!wallet) return + wallet.on('connect', async () => { + setWalletStore((state) => { + state.connected = true + }) + notify({ + message: 'Wallet connected', + description: + 'Connected to wallet ' + + wallet.publicKey.toString().substr(0, 5) + + '...' + + wallet.publicKey.toString().substr(-5), + }) + await actions.fetchWalletTokenAccounts() + await actions.fetchWalletMints() + }) + wallet.on('disconnect', () => { + setWalletStore((state) => { + state.connected = false + state.tokenAccounts = [] + state.mints = {} + }) + notify({ + type: 'info', + message: 'Disconnected from wallet', + }) + }) + return () => { + if (wallet && wallet.connected) { + wallet.disconnect() + } + setWalletStore((state) => { + state.connected = false + }) + } + }, [wallet, setWalletStore]) + + useInterval(async () => { + await actions.fetchWalletTokenAccounts() + await actions.fetchWalletMints() + }, 20 * SECONDS) + + return { connected, wallet } +} diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..1cd5de2 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,17 @@ +module.exports = { + roots: [''], + moduleFileExtensions: ['ts', 'tsx', 'js', 'json', 'jsx'], + testPathIgnorePatterns: ['[/\\\\](node_modules|.next)[/\\\\]'], + transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(ts|tsx)$'], + transform: { + '^.+\\.(ts|tsx)$': 'babel-jest', + }, + watchPlugins: [ + 'jest-watch-typeahead/filename', + 'jest-watch-typeahead/testname', + ], + moduleNameMapper: { + '\\.(css|less|sass|scss)$': 'identity-obj-proxy', + '\\.(gif|ttf|eot|svg|png)$': '/test/__mocks__/fileMock.js', + }, +} diff --git a/netlify.toml b/netlify.toml index 3260211..6759042 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,7 +1,7 @@ - [build] - command = "npm run build && npm run export" - publish = "out" +command = "npm run build" +publish = "out" [[plugins]] - package = "@netlify/plugin-nextjs" \ No newline at end of file +package = "@netlify/plugin-nextjs" + diff --git a/next.config.js b/next.config.js index 42d0abc..faceb0f 100644 --- a/next.config.js +++ b/next.config.js @@ -1,14 +1,11 @@ -/* eslint-disable */ -const withAntdLess = require("next-plugin-antd-less"); - -module.exports = withAntdLess({ - // optional - lessVarsFilePath: "./styles/theme.less", - cssLoaderOptions: {}, - - // Other Config Here... - +module.exports = { + target: 'serverless', webpack(config) { - return config; + config.module.rules.push({ + test: /\.svg$/, + use: ['@svgr/webpack'], + }) + + return config }, -}); +} diff --git a/package.json b/package.json index e7508c0..ff0a11c 100644 --- a/package.json +++ b/package.json @@ -1,36 +1,61 @@ { - "name": "with-typescript", + "name": "with-typescript-eslint-jest", + "author": "@erikdstock", + "license": "MIT", "version": "1.0.0", "scripts": { - "dev": "next", + "dev": "next dev", "build": "next build", "start": "next start", - "export": "next export", - "type-check": "tsc" + "prepare": "husky install", + "tc": "yarn type-check --watch", + "type-check": "tsc --pretty --noEmit", + "format": "prettier --write .", + "lint": "eslint . --ext ts --ext tsx --ext js --ext jsx", + "test": "jest", + "test-all": "yarn lint && yarn type-check && yarn test" + }, + "lint-staged": { + "*.@(ts|tsx|js|jsx)": [ + "yarn format" + ] }, "dependencies": { - "@blockworks-foundation/mango-client": "https://github.com/blockworks-foundation/mango-client-ts#5_tokens", - "antd": "^4.12.3", - "autoprefixer": "^10.2.4", - "babel-plugin-import": "^1.13.3", + "@emotion/react": "^11.1.5", + "@emotion/styled": "^11.3.0", + "@headlessui/react": "^1.0.0", + "@heroicons/react": "^1.0.1", + "@project-serum/sol-wallet-adapter": "^0.2.0", + "@solana/spl-token": "^0.1.3", + "@solana/web3.js": "^1.5.0", + "immer": "^9.0.1", "next": "latest", - "next-plugin-antd-less": "^0.3.0", - "postcss": "^8.2.6", - "react": "^16.12.0", - "react-dom": "^16.12.0", - "styled-components": "^5.2.1", - "tailwindcss": "^2.0.3" + "next-themes": "^0.0.14", + "react": "^17.0.1", + "react-dom": "^17.0.1", + "zustand": "^3.4.1" }, "devDependencies": { - "@types/node": "^12.12.21", - "@types/react": "^16.9.16", - "@types/react-dom": "^16.9.4", - "@types/styled-components": "^5.1.7", - "typescript": "4.0", - "webpack": "^4.44.2" - }, - "prettier": { - "printWidth": 100 - }, - "license": "MIT" + "@testing-library/react": "^11.2.5", + "@types/jest": "^26.0.20", + "@types/node": "^14.14.25", + "@types/react": "^17.0.1", + "@typescript-eslint/eslint-plugin": "^4.14.2", + "@typescript-eslint/parser": "^4.14.2", + "babel-jest": "^26.6.3", + "eslint": "^7.19.0", + "eslint-config-prettier": "^7.2.0", + "eslint-plugin-react": "^7.19.0", + "eslint-plugin-react-hooks": "^4.2.0", + "husky": "^6.0.0", + "identity-obj-proxy": "^3.0.0", + "jest": "^26.6.3", + "jest-watch-typeahead": "^0.6.1", + "lint-staged": "^10.0.10", + "postcss": "^8.2.12", + "postcss-preset-env": "^6.7.0", + "prettier": "^2.0.2", + "tailwindcss": "^2.1.2", + "typescript": "^4.1.3" + } } diff --git a/pages/_app.tsx b/pages/_app.tsx index fe68a98..b38e10f 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,5 +1,45 @@ -import "tailwindcss/tailwind.css"; -// This default export is required in a new `pages/_app.js` file. -export default function MyApp({ Component, pageProps }) { - return ; +import Head from 'next/head' +import { ThemeProvider } from 'next-themes' +import '../styles/index.css' +import useWallet from '../hooks/useWallet' + +function App({ Component, pageProps }) { + useWallet() + + const title = 'Mango Markets' + const description = + 'Mango Markets - Decentralised, cross-margin trading up to 5x leverage with lightning speed and near-zero fees powered by Serum.' + const keywords = + 'Mango Markets, Serum, SRM, Serum DEX, DEFI, Decentralized Finance, Decentralised Finance, Crypto, ERC20, Ethereum, Decentralize, Solana, SOL, SPL, Cross-Chain, Trading, Fastest, Fast, SerumBTC, SerumUSD, SRM Tokens, SPL Tokens' + + return ( + <> + + {title} + + + + + + + + + + + + + + + + + + + + + ) } + +export default App diff --git a/pages/api/hello.ts b/pages/api/hello.ts new file mode 100644 index 0000000..b3be12c --- /dev/null +++ b/pages/api/hello.ts @@ -0,0 +1,9 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction + +import { NextApiRequest, NextApiResponse } from 'next' + +const handler = (req: NextApiRequest, res: NextApiResponse) => { + res.status(200).json({ name: 'John Doe' }) +} + +export default handler diff --git a/pages/careers.tsx b/pages/careers.tsx deleted file mode 100644 index ea3e67a..0000000 --- a/pages/careers.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { Divider, Layout, Row, Col } from "antd"; -import Head from "next/head"; -import { CSSProperties } from "react"; -import ContactIcons from "../components/ContactIcons"; -import Logo from "../components/Logo"; -import { Navigation } from "../components/Navigation"; - -const { Header, Footer, Content } = Layout; - -export interface ButtonStyle extends CSSProperties { - "-webkit-font-smoothing": string; -} - -const CareersPage = () => ( - <> - - Mango Markets - - - - - - -