share position modal

This commit is contained in:
tjs 2022-02-08 13:50:44 -05:00
parent 0ddcf8c2c2
commit f4ba160686
12 changed files with 415 additions and 81 deletions

View File

@ -8,7 +8,7 @@ const MenuItem = ({ href, children, newWindow = false }) => {
return (
<Link href={href} shallow={true}>
<a
className={`border-b border-th-bkg-4 md:border-none flex justify-between text-th-fgd-1 font-bold items-center md:px-1 py-3 md:py-0 hover:text-th-primary
className={`h-full border-b border-th-bkg-4 md:border-none flex justify-between text-th-fgd-1 font-bold items-center md:px-2 lg:px-4 py-3 md:py-0 hover:text-th-primary
${asPath === href ? `text-th-primary` : `border-transparent`}
`}
target={newWindow ? '_blank' : ''}

View File

@ -1,14 +1,18 @@
import React from 'react'
import { Portal } from 'react-portal'
import { XIcon } from '@heroicons/react/outline'
const Modal = ({
isOpen,
onClose,
children,
hideClose = false,
noPadding = false,
alignTop = false,
}) => {
const Modal: any = React.forwardRef<any, any>((props, ref) => {
const {
isOpen,
onClose,
children,
hideClose = false,
noPadding = false,
alignTop = false,
className = '',
} = props
return (
<Portal>
<div
@ -40,7 +44,8 @@ const Modal = ({
className={`inline-block bg-th-bkg-2 min-h-screen sm:min-h-full
sm:rounded-lg text-left ${
noPadding ? '' : 'px-8 pt-6 pb-6'
} shadow-lg transform transition-all align-middle sm:max-w-md w-full`}
} shadow-lg transform transition-all align-middle sm:max-w-md w-full ${className}`}
ref={ref}
>
{!hideClose ? (
<div className="">
@ -59,7 +64,7 @@ const Modal = ({
</div>
</Portal>
)
}
})
const Header = ({ children }) => {
return (

View File

@ -1,9 +1,17 @@
import { useRef, useState } from 'react'
import { Popover } from '@headlessui/react'
import { DotsHorizontalIcon } from '@heroicons/react/solid'
import Link from 'next/link'
import { ChevronDownIcon } from '@heroicons/react/outline'
export default function NavDropMenu({ menuTitle = '', linksArray = [] }) {
type NavDropMenuProps = {
menuTitle: string | React.ReactNode
linksArray: [string, string, boolean][]
}
export default function NavDropMenu({
menuTitle = '',
linksArray = [],
}: NavDropMenuProps) {
const buttonRef = useRef(null)
const [openState, setOpenState] = useState(false)
@ -34,20 +42,23 @@ export default function NavDropMenu({ menuTitle = '', linksArray = [] }) {
onMouseLeave={() => onHover(open, 'onMouseLeave')}
className="flex flex-col"
>
<Popover.Button className="h-10 focus:outline-none" ref={buttonRef}>
<Popover.Button
className="h-10 text-th-fgd-1 hover:text-th-primary md:px-2 lg:px-4 focus:outline-none"
ref={buttonRef}
>
<div
className="flex items-center text-th-fgd-1 hover:text-th-primary"
className="flex items-center"
onClick={() => handleClick(open)}
>
<span className="font-bold">{menuTitle}</span>
<DotsHorizontalIcon
<ChevronDownIcon
className="h-4 w-4 default-transition ml-1.5"
aria-hidden="true"
/>
</div>
</Popover.Button>
<Popover.Panel className="absolute top-10 z-10">
<div className="relative bg-th-bkg-2 divide-y divide-th-bkg-3 px-4 rounded">
<div className="relative bg-th-bkg-1 divide-y divide-th-bkg-3 px-4 rounded">
{linksArray.map(([name, href, isExternal]) =>
!isExternal ? (
<Link href={href} key={href}>

View File

@ -5,7 +5,7 @@ import { useTranslation } from 'next-i18next'
import { ExclamationIcon } from '@heroicons/react/outline'
import useMangoStore from '../stores/useMangoStore'
import Button from '../components/Button'
import Button, { LinkButton } from '../components/Button'
import { useViewport } from '../hooks/useViewport'
import { breakpoints } from './TradePageGrid'
import { ExpandableRow, Table, Td, Th, TrBody, TrHead } from './TableElements'
@ -16,16 +16,19 @@ import PerpSideBadge from './PerpSideBadge'
import PnlText from './PnlText'
import { settlePnl } from './MarketPosition'
import MobileTableHeader from './mobile/MobileTableHeader'
import ShareModal from './ShareModal'
import { TwitterIcon } from './icons'
import { marketSelector } from '../stores/selectors'
const PositionsTable = () => {
const { t } = useTranslation('common')
const { reloadMangoAccount } = useMangoStore((s) => s.actions)
const [settling, setSettling] = useState(false)
const selectedMarket = useMangoStore((s) => s.selectedMarket.current)
const selectedMarketConfig = useMangoStore((s) => s.selectedMarket.config)
const price = useMangoStore((s) => s.tradeForm.price)
const [showShareModal, setShowShareModal] = useState(false)
const [showMarketCloseModal, setShowMarketCloseModal] = useState(false)
const market = useMangoStore(marketSelector)
const price = useMangoStore((s) => s.tradeForm.price)
const setMangoStore = useMangoStore((s) => s.set)
const openPositions = useMangoStore(
(s) => s.selectedMangoAccount.openPerpPositions
@ -41,7 +44,7 @@ const PositionsTable = () => {
}, [])
const handleSizeClick = (size, side, indexPrice) => {
const step = selectedMarket.minOrderSize
const step = market.minOrderSize
const priceOrDefault = price ? price : indexPrice
const roundedSize = Math.round(size / step) * step
const quoteSize = roundedSize * priceOrDefault
@ -62,6 +65,10 @@ const PositionsTable = () => {
setSettling(false)
}
const handleCloseShare = useCallback(() => {
setShowShareModal(false)
}, [])
return (
<div className="flex flex-col pb-2">
{unsettledPositions.length > 0 ? (
@ -170,7 +177,7 @@ const PositionsTable = () => {
</Td>
<Td>
{basePosition &&
selectedMarketConfig.kind === 'perp' &&
marketConfig.kind === 'perp' &&
asPath.includes(marketConfig.baseSymbol) ? (
<span
className="cursor-pointer underline hover:no-underline"
@ -212,6 +219,14 @@ const PositionsTable = () => {
'--'
)}
</Td>
<Td>
<LinkButton
onClick={() => setShowShareModal(true)}
disabled={!avgEntryPrice}
>
<TwitterIcon className="h-4 w-4" />
</LinkButton>
</Td>
{showMarketCloseModal ? (
<MarketCloseModal
isOpen={showMarketCloseModal}
@ -220,6 +235,20 @@ const PositionsTable = () => {
marketIndex={marketConfig.marketIndex}
/>
) : null}
{showShareModal ? (
<ShareModal
isOpen={showShareModal}
onClose={handleCloseShare}
position={{
marketConfig,
indexPrice,
avgEntryPrice: avgEntryPrice
? avgEntryPrice
: '0.00',
basePosition,
}}
/>
) : null}
</TrBody>
)
}

181
components/ShareModal.tsx Normal file
View File

@ -0,0 +1,181 @@
import {
FunctionComponent,
useEffect,
useMemo,
createRef,
useState,
} from 'react'
import { getWeights, MarketConfig } from '@blockworks-foundation/mango-client'
import useMangoStore from '../stores/useMangoStore'
import Modal from './Modal'
import { useScreenshot } from '../hooks/useScreenshot'
import { ExternalLinkIcon } from '@heroicons/react/outline'
interface ShareModalProps {
onClose: () => void
isOpen: boolean
position: {
indexPrice: number
avgEntryPrice: number
basePosition: number
marketConfig: MarketConfig
}
}
const ShareModal: FunctionComponent<ShareModalProps> = ({
isOpen,
onClose,
position,
}) => {
const ref = createRef()
const [copied, setCopied] = useState(false)
const [showButton, setShowButton] = useState(true)
const marketConfig = position.marketConfig
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const [image, takeScreenshot] = useScreenshot()
const initLeverage = useMemo(() => {
if (!mangoGroup || !marketConfig) return 1
const ws = getWeights(mangoGroup, marketConfig.marketIndex, 'Init')
return Math.round((100 * -1) / (ws.perpAssetWeight.toNumber() - 1)) / 100
}, [mangoGroup, marketConfig])
const positionPercentage =
((position.indexPrice - position.avgEntryPrice) / position.avgEntryPrice) *
100 *
initLeverage
const side = position.basePosition > 0 ? 'LONG' : 'SHORT'
async function copyToClipboard(image) {
try {
image.toBlob((blob) => {
navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })])
}, 'image/png')
} catch (error) {
console.error(error)
}
}
useEffect(() => {
if (image) {
copyToClipboard(image)
setCopied(true)
setShowButton(true)
}
}, [image])
useEffect(() => {
// if the button is hidden we are taking a screenshot
if (!showButton) {
takeScreenshot(ref.current)
}
}, [showButton])
const handleCopyToClipboard = () => {
setShowButton(false)
}
const isProfit = positionPercentage > 0
return (
<Modal
isOpen={isOpen}
onClose={onClose}
className="md:max-w-sm"
noPadding
hideClose
ref={ref}
>
<div className="relative overflow-hidden px-8 pt-6 pb-6 rounded-lg">
<div id="share-image" className="relative z-20">
<div className="flex justify-center p-4 pt-0">
<img
className={`h-32 w-auto`}
src="/assets/icons/logo.svg"
alt="next"
/>
</div>
<div className="text-th-fgd-1 text-center text-xl">
<span className="font-bold">Mango</span>
<span className="font-extralight"> Markets</span>
</div>
<div className="pb-4 text-lg text-center">
<span className="text-th-fgd-3">{position.marketConfig.name}</span>
<span className="px-2 text-th-fgd-4">|</span>
<span
className={`${
position.basePosition > 0 ? 'text-th-green' : 'text-th-red'
}`}
>
{side}
</span>
</div>
<div className="flex justify-center items-center">
<div
className={`border mb-6 px-4 ${
!showButton ? 'pt-2' : 'py-2'
} rounded-lg text-5xl text-center font-light ${
isProfit
? 'border-th-green text-th-green'
: 'border-th-red text-th-red'
}`}
>
{isProfit
? `${positionPercentage.toFixed(2)}`
: positionPercentage.toFixed(2)}
%
</div>
</div>
<div className="space-y-2 text-th-fgd-1">
<div className="flex items-center justify-between">
<span className="text-th-fgd-2">Avg Entry Price:</span>
<span>${position.avgEntryPrice.toFixed(2)}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-th-fgd-2">Mark Price:</span>
<span>${position.indexPrice.toFixed(2)}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-th-fgd-2">Max Leverage:</span>
<span>{initLeverage}x</span>
</div>
</div>
{copied ? (
<a
className="bg-th-bkg-3 hover:cursor-pointer block mt-6 px-4 py-3 rounded-full text-center text-th-fgd-1 w-full"
href={`https://twitter.com/intent/tweet?text=I'm ${side} %24${position.marketConfig.baseSymbol} perp on %40mangomarkets%0A[PASTE IMAGE HERE]`}
target="_blank"
rel="noreferrer"
>
<div className="flex items-center justify-center">
<div>Tweet</div>
<ExternalLinkIcon className="ml-1 h-4 w-4" />
</div>
</a>
) : (
<a
className={`${
!showButton ? 'hidden' : ''
} bg-th-bkg-3 hover:cursor-pointer block mt-6 px-4 py-3 rounded-full text-center text-th-fgd-1 w-full`}
onClick={handleCopyToClipboard}
>
Copy Image to Clipboard & Share
</a>
)}
{!showButton ? <div className="mb-6">.</div> : null}
</div>
<div
className={`absolute h-full w-full opacity-75 bottom-2/3 left-0 pointer-events-none bg-gradient-to-b ${
isProfit ? 'from-th-green' : 'from-th-red'
} to-th-bkg-2 z-10`}
></div>
</div>
</Modal>
)
}
export default ShareModal

View File

@ -13,11 +13,11 @@ import { DEFAULT_MARKET_KEY, initialMarket } from './SettingsModal'
import { useTranslation } from 'next-i18next'
import Settings from './Settings'
const StyledNewLabel = ({ children, ...props }) => (
<div style={{ fontSize: '0.5rem', marginLeft: '1px' }} {...props}>
{children}
</div>
)
// const StyledNewLabel = ({ children, ...props }) => (
// <div style={{ fontSize: '0.5rem', marginLeft: '1px' }} {...props}>
// {children}
// </div>
// )
const TopBar = () => {
const { t } = useTranslation('common')
@ -50,58 +50,22 @@ const TopBar = () => {
/>
</div>
</Link>
<div
className={`hidden md:flex md:items-center md:space-x-4 lg:space-x-6 md:ml-4`}
>
<div className={`hidden md:flex md:items-center md:ml-4`}>
<MenuItem href={defaultMarket.path}>{t('trade')}</MenuItem>
<MenuItem href="/swap">{t('swap')}</MenuItem>
<MenuItem href="/account">{t('account')}</MenuItem>
<MenuItem href="/borrow">{t('borrow')}</MenuItem>
<div className="relative">
<MenuItem href="/risk-calculator">
{t('calculator')}
<div>
<div className="absolute flex items-center justify-center h-4 px-1.5 bg-gradient-to-br from-red-500 to-yellow-500 rounded-full -right-5 -top-3">
<StyledNewLabel className="text-white uppercase">
new
</StyledNewLabel>
</div>
</div>
</MenuItem>
</div>
<MenuItem href="/stats">{t('stats')}</MenuItem>
<MenuItem href="https://docs.mango.markets/" newWindow>
{t('learn')}
</MenuItem>
<NavDropMenu
menuTitle={t('more')}
// linksArray: [name: string, href: string, isExternal: boolean]
linksArray={[
['Mango v1', 'https://v1.mango.markets', true],
[t('calculator'), '/risk-calculator', false],
[t('learn'), 'https://docs.mango.markets/', true],
['Mango v2', 'https://v2.mango.markets', true],
['Mango v1', 'https://v1.mango.markets', true],
]}
/>
{/* <button
onClick={() => {
handleLocaleChange('en')
}}
>
English
</button>
<button
onClick={() => {
handleLocaleChange('zh')
}}
>
</button>
<button
onClick={() => {
handleLocaleChange('zh_tw')
}}
>
</button> */}
</div>
</div>
<div className="flex items-center">

View File

@ -121,6 +121,24 @@ export const TelegramIcon = ({ className }) => {
)
}
export const TwitterIcon = ({ className }) => {
return (
<svg
className={`${className}`}
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
>
<path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path>
</svg>
)
}
export const ProfileIcon = ({ className }) => {
return (
<svg

75
hooks/useScreenshot.tsx Normal file
View File

@ -0,0 +1,75 @@
import { useState } from 'react'
import html2canvas from 'html2canvas'
/**
* @module Main_Hook
* Hook return
* @typedef {Array} HookReturn
* @property {string} HookReturn[0] - image string
* @property {string} HookReturn[1] - take screen shot string
* @property {object} HookReturn[2] - errors
*/
/**
* hook for creating screenshot from html node
* @returns {HookReturn}
*/
const useScreenshot = () => {
const [image, setImage] = useState(null)
const [error, setError] = useState(null)
/**
* convert html node to image
* @param {HTMLElement} node
*/
const takeScreenShot = (node) => {
if (!node) {
throw new Error('You should provide correct html node.')
}
return html2canvas(node)
.then((canvas) => {
const croppedCanvas = document.createElement('canvas')
const croppedCanvasContext = croppedCanvas.getContext('2d')
// init data
const cropPositionTop = 0
const cropPositionLeft = 0
const cropWidth = canvas.width
const cropHeight = canvas.height
croppedCanvas.width = cropWidth
croppedCanvas.height = cropHeight
croppedCanvasContext.drawImage(
canvas,
cropPositionLeft,
cropPositionTop
)
setImage(croppedCanvas)
return croppedCanvas
})
.catch(setError)
}
return [
image,
takeScreenShot,
{
error,
},
]
}
/**
* creates name of file
* @param {string} extension
* @param {string[]} parts of file name
*/
const createFileName = (extension = '', ...names) => {
if (!extension) {
return ''
}
return `${names.join('')}.${extension}`
}
export { useScreenshot, createFileName }

View File

@ -38,6 +38,7 @@
"buffer-layout": "^1.2.0",
"dayjs": "^1.10.4",
"export-to-csv": "^0.2.1",
"html2canvas": "^1.4.1",
"immer": "^9.0.1",
"intro.js": "^4.2.2",
"intro.js-react": "^0.5.0",

View File

@ -172,7 +172,7 @@
"languages-tip-title": "Multilingual?",
"layout-tip-desc": "Unlock to re-arrange and re-size the trading panels to your liking.",
"layout-tip-title": "Customize Layout",
"learn": "Learn",
"learn": "Documentation",
"learn-more": "Learn more",
"lets-go": "Let's Go",
"leverage": "Leverage",

View File

@ -522,14 +522,28 @@ const useMangoStore = create<MangoStore>((set, get) => {
const set = get().set
if (!selectedMangoAccount) return
let serumTradeHistory = []
fetch(
`https://event-history-api.herokuapp.com/perp_trades/${selectedMangoAccount.publicKey.toString()}`
)
.then((response) => response.json())
.then((jsonPerpHistory) => {
const perpHistory = jsonPerpHistory?.data || []
set((state) => {
state.tradeHistory = [...state.tradeHistory, ...perpHistory]
})
})
.catch((e) => {
console.error('Error fetching trade history', e)
})
if (selectedMangoAccount.spotOpenOrdersAccounts.length) {
const openOrdersAccounts =
selectedMangoAccount.spotOpenOrdersAccounts.filter(isDefined)
const publicKeys = openOrdersAccounts.map((act) =>
act.publicKey.toString()
)
serumTradeHistory = await Promise.all(
Promise.all(
publicKeys.map(async (pk) => {
const response = await fetch(
`https://event-history-api.herokuapp.com/trades/open_orders/${pk.toString()}`
@ -538,16 +552,18 @@ const useMangoStore = create<MangoStore>((set, get) => {
return parsedResponse?.data ? parsedResponse.data : []
})
)
.then((serumTradeHistory) => {
set((state) => {
state.tradeHistory = [
...serumTradeHistory,
...state.tradeHistory,
]
})
})
.catch((e) => {
console.error('Error fetching trade history', e)
})
}
const perpHistory = await fetch(
`https://event-history-api.herokuapp.com/perp_trades/${selectedMangoAccount.publicKey.toString()}`
)
let parsedPerpHistory = await perpHistory.json()
parsedPerpHistory = parsedPerpHistory?.data || []
set((state) => {
state.tradeHistory = [...serumTradeHistory, ...parsedPerpHistory]
})
},
async reloadMangoAccount() {
const set = get().set

View File

@ -3123,6 +3123,11 @@ base-x@^3.0.2:
dependencies:
safe-buffer "^5.0.1"
base64-arraybuffer@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
base64-js@^1.0.2, base64-js@^1.3.1, base64-js@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@ -3898,6 +3903,13 @@ css-has-pseudo@^3.0.2:
dependencies:
postcss-selector-parser "^6.0.8"
css-line-break@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/css-line-break/-/css-line-break-2.1.0.tgz#bfef660dfa6f5397ea54116bb3cb4873edbc4fa0"
integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==
dependencies:
utrie "^1.0.2"
css-prefers-color-scheme@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.2.tgz#d5c03a980caab92d8beeee176a8795d331e0c727"
@ -5257,6 +5269,14 @@ html-tags@^3.1.0:
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140"
integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==
html2canvas@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.4.1.tgz#7cef1888311b5011d507794a066041b14669a543"
integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==
dependencies:
css-line-break "^2.1.0"
text-segmentation "^1.0.3"
http-errors@1.7.3:
version "1.7.3"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
@ -9168,6 +9188,13 @@ text-encoding-utf-8@^1.0.2:
resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13"
integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==
text-segmentation@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/text-segmentation/-/text-segmentation-1.0.3.tgz#52a388159efffe746b24a63ba311b6ac9f2d7943"
integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==
dependencies:
utrie "^1.0.2"
text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@ -9505,6 +9532,13 @@ util@0.12.4, util@^0.12.0:
safe-buffer "^5.1.2"
which-typed-array "^1.1.2"
utrie@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/utrie/-/utrie-1.0.2.tgz#d42fe44de9bc0119c25de7f564a6ed1b2c87a645"
integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==
dependencies:
base64-arraybuffer "^1.0.2"
uuid@^8.3.0, uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"