form validation

This commit is contained in:
saml33 2023-06-23 13:37:34 +10:00
parent 8ed973a063
commit 6337b696e7
5 changed files with 314 additions and 137 deletions

View File

@ -59,7 +59,11 @@ const fetchFundingTotals = async (mangoAccountPk: string) => {
const stats: TotalAccountFundingItem[] = entries
.map(([key, value]) => {
return { ...value, market: key }
return {
long_funding: value.long_funding * -1,
short_funding: value.short_funding * -1,
market: key,
}
})
.filter((x) => x)
@ -208,7 +212,7 @@ const AccountPage = () => {
const interestTotalValue = useMemo(() => {
if (totalInterestData.length) {
return totalInterestData.reduce(
(a, c) => a + c.borrow_interest_usd + c.deposit_interest_usd,
(a, c) => a + (c.borrow_interest_usd * -1 + c.deposit_interest_usd),
0
)
}

View File

@ -2,20 +2,21 @@ import ButtonGroup from '@components/forms/ButtonGroup'
import Checkbox from '@components/forms/Checkbox'
import Input from '@components/forms/Input'
import Label from '@components/forms/Label'
import Button, { LinkButton } from '@components/shared/Button'
import Button, { IconButton, LinkButton } from '@components/shared/Button'
import InlineNotification from '@components/shared/InlineNotification'
import Modal from '@components/shared/Modal'
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
import Tooltip from '@components/shared/Tooltip'
import { KeyIcon } from '@heroicons/react/20/solid'
import { KeyIcon, TrashIcon } from '@heroicons/react/20/solid'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { useTranslation } from 'next-i18next'
import { useCallback, useState } from 'react'
import { useState } from 'react'
import { ModalProps } from 'types/modal'
import { HOT_KEYS_KEY } from 'utils/constants'
export type HotKey = {
ioc: boolean
keySequence: string
// market: string
margin: boolean
orderSide: 'buy' | 'sell'
orderSizeType: 'percentage' | 'notional'
@ -28,25 +29,96 @@ export type HotKey = {
const HotKeysSettings = () => {
const { t } = useTranslation('settings')
const [hotKeys] = useLocalStorageState(HOT_KEYS_KEY, [])
const [hotKeys, setHotKeys] = useLocalStorageState(HOT_KEYS_KEY, [])
const [showHotKeyModal, setShowHotKeyModal] = useState(false)
const handleDeleteKey = (key: string) => {
const newKeys = hotKeys.filter((hk: HotKey) => hk.keySequence !== key)
setHotKeys([...newKeys])
}
return (
<>
<div className="flex items-center justify-between">
<h2 className="mb-1 text-base">{t('hot-keys')}</h2>
<LinkButton onClick={() => setShowHotKeyModal(true)}>
Create New Hot Key
</LinkButton>
{hotKeys.length ? (
<LinkButton onClick={() => setShowHotKeyModal(true)}>
{t('create-new-key')}
</LinkButton>
) : null}
</div>
<p className="mb-4">{t('hot-keys-desc')}</p>
{hotKeys.length ? (
hotKeys.map((k: HotKey) => (
<div key={k.keySequence}>
<p>{k.keySequence}</p>
</div>
))
<Table>
<thead>
<TrHead>
<Th className="text-left">{t('key')}</Th>
<Th className="text-right">{t('order-type')}</Th>
<Th className="text-right">{t('side')}</Th>
<Th className="text-right">{t('size')}</Th>
<Th className="text-right">{t('price')}</Th>
<Th className="text-right">{t('options')}</Th>
<Th />
</TrHead>
</thead>
<tbody>
{hotKeys.map((hk: HotKey) => {
const {
keySequence,
orderSide,
orderPrice,
orderSize,
orderSizeType,
orderType,
ioc,
margin,
reduceOnly,
postOnly,
} = hk
const size =
orderSizeType === 'percentage'
? `${orderSize}% of max`
: `$${orderSize}`
const price = orderPrice ? `${orderPrice}% from oracle` : 'market'
const options = {
margin: margin,
IOC: ioc,
post: postOnly,
reduce: reduceOnly,
}
return (
<TrBody key={keySequence} className="text-right">
<Td className="text-left">{keySequence}</Td>
<Td className="text-right">{orderType}</Td>
<Td className="text-right">{orderSide}</Td>
<Td className="text-right">{size}</Td>
<Td className="text-right">{price}</Td>
<Td className="text-right">
{Object.entries(options).map((e) => {
return e[1]
? `${e[0] !== 'margin' ? ', ' : ''}${e[0]}`
: ''
})}
</Td>
<Td>
<div className="flex justify-end">
<IconButton
onClick={() => handleDeleteKey(keySequence)}
size="small"
>
<TrashIcon className="h-4 w-4" />
</IconButton>
</div>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
) : (
<div className="mb-8 rounded-lg border border-th-bkg-3 p-6">
<div className="rounded-lg border border-th-bkg-3 p-6">
<div className="flex flex-col items-center">
<KeyIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p className="mb-4">{t('no-hot-keys')}</p>
@ -68,151 +140,243 @@ const HotKeysSettings = () => {
export default HotKeysSettings
// add ioc, postOnly and reduceOnly checkboxes
type FormErrors = Partial<Record<keyof HotKeyForm, string>>
type HotKeyForm = {
baseKey: string
triggerKey: string
price: string
side: 'buy' | 'sell'
size: string
sizeType: 'percentage' | 'notional'
orderType: 'limit' | 'market'
ioc: boolean
post: boolean
margin: boolean
reduce: boolean
}
const DEFAULT_FORM_VALUES: HotKeyForm = {
baseKey: 'shift',
triggerKey: '',
price: '',
side: 'buy',
size: '',
sizeType: 'percentage',
orderType: 'limit',
ioc: false,
post: false,
margin: false,
reduce: false,
}
const HotKeyModal = ({ isOpen, onClose }: ModalProps) => {
const { t } = useTranslation(['settings', 'trade'])
const [hotKeys, setHotKeys] = useLocalStorageState<HotKey[]>(HOT_KEYS_KEY, [])
// const perpMarkets = mangoStore((s) => s.perpMarkets)
// const serumMarkets = mangoStore((s) => s.serumMarkets)
// const allMarkets =
// perpMarkets.length && serumMarkets.length
// ? [
// 'All',
// ...perpMarkets.map((m) => m.name),
// ...serumMarkets.map((m) => m.name),
// ]
// : ['All']
const [keySequence, setKeySequence] = useState('')
// const [market, setMarket] = useState('All')
const [orderPrice, setOrderPrice] = useState('')
const [orderSide, setOrderSide] = useState('buy')
const [orderSizeType, setOrderSizeType] = useState('percentage')
const [orderSize, setOrderSize] = useState('')
const [orderType, setOrderType] = useState('limit')
const [postOnly, setPostOnly] = useState(false)
const [ioc, setIoc] = useState(false)
const [margin, setMargin] = useState(false)
const [reduceOnly, setReduceOnly] = useState(false)
const [hotKeyForm, setHotKeyForm] = useState<HotKeyForm>({
...DEFAULT_FORM_VALUES,
})
const [formErrors, setFormErrors] = useState<FormErrors>({})
const handlePostOnlyChange = useCallback(
(postOnly: boolean) => {
let updatedIoc = ioc
if (postOnly) {
updatedIoc = !postOnly
}
setPostOnly(postOnly)
setIoc(updatedIoc)
},
[ioc]
)
const handleSetForm = (propertyName: string, value: string | boolean) => {
setFormErrors({})
setHotKeyForm((prevState) => ({ ...prevState, [propertyName]: value }))
}
const handleIocChange = useCallback(
(ioc: boolean) => {
let updatedPostOnly = postOnly
if (ioc) {
updatedPostOnly = !ioc
const handlePostOnlyChange = (postOnly: boolean) => {
if (postOnly) {
handleSetForm('ioc', !postOnly)
}
handleSetForm('post', postOnly)
}
const handleIocChange = (ioc: boolean) => {
if (ioc) {
handleSetForm('post', !ioc)
}
handleSetForm('ioc', ioc)
}
const isFormValid = (form: HotKeyForm) => {
const invalidFields: FormErrors = {}
setFormErrors({})
const triggerKey: (keyof HotKeyForm)[] = ['triggerKey']
const requiredFields: (keyof HotKeyForm)[] = ['size', 'price', 'triggerKey']
const numberFields: (keyof HotKeyForm)[] = ['size', 'price']
const alphanumericRegex = /^[a-zA-Z0-9]+$/
for (const key of triggerKey) {
const value = form[key] as string
if (value.length > 1) {
invalidFields[key] = t('error-too-many-characters')
}
setPostOnly(updatedPostOnly)
setIoc(ioc)
},
[postOnly]
)
if (!alphanumericRegex.test(value)) {
invalidFields[key] = t('error-alphanumeric-only')
}
}
for (const key of requiredFields) {
const value = form[key] as string
if (!value) {
if (hotKeyForm.orderType === 'market') {
console.log(key, invalidFields[key])
if (key !== 'price') {
invalidFields[key] = t('error-required-field')
}
} else {
invalidFields[key] = t('error-required-field')
}
}
}
for (const key of numberFields) {
const value = form[key] as string
if (value) {
if (isNaN(parseFloat(value))) {
invalidFields[key] = t('error-must-be-number')
}
if (parseFloat(value) < 0) {
invalidFields[key] = t('error-must-be-above-zero')
}
if (parseFloat(value) > 100) {
if (key === 'price') {
invalidFields[key] = t('error-must-be-below-100')
} else {
if (hotKeyForm.sizeType === 'percentage') {
invalidFields[key] = t('error-must-be-below-100')
}
}
}
}
}
if (Object.keys(invalidFields).length) {
setFormErrors(invalidFields)
}
return invalidFields
}
const handleSave = () => {
const invalidFields = isFormValid(hotKeyForm)
if (Object.keys(invalidFields).length) {
return
}
const newHotKey = {
keySequence: keySequence,
// market: market,
orderSide: orderSide,
orderSizeType: orderSizeType,
orderSize: orderSize,
orderType: orderType,
orderPrice: orderPrice,
ioc,
margin,
postOnly,
reduceOnly,
keySequence: `${hotKeyForm.baseKey}+${hotKeyForm.triggerKey}`,
orderSide: hotKeyForm.side,
orderSizeType: hotKeyForm.sizeType,
orderSize: hotKeyForm.size,
orderType: hotKeyForm.orderType,
orderPrice: hotKeyForm.price,
ioc: hotKeyForm.ioc,
margin: hotKeyForm.margin,
postOnly: hotKeyForm.post,
reduceOnly: hotKeyForm.reduce,
}
setHotKeys([...hotKeys, newHotKey])
onClose()
}
const disabled =
!keySequence || (orderType === 'limit' && !orderPrice) || !orderSize
return (
<Modal isOpen={isOpen} onClose={onClose}>
<>
<h2 className="mb-4 text-center">{t('create-hot-key')}</h2>
<div className="mb-4">
<Label text={t('key-sequence')} />
<Input
type="text"
value={keySequence}
onChange={(e) => setKeySequence(e.target.value)}
<Label text={t('base-key')} />
<ButtonGroup
activeValue={hotKeyForm.baseKey}
onChange={(key) => handleSetForm('baseKey', key)}
values={['shift', 'ctrl', 'option']}
/>
</div>
{/* <div className="mb-4">
<Label text={t('market')} />
<Select
value={market}
onChange={(market) => setMarket(market)}
className="w-full"
>
{allMarkets.map((market) => (
<Select.Option key={market} value={market}>
<div className="w-full">{market}</div>
</Select.Option>
))}
</Select>
</div> */}
<div className="mb-4">
<Label text={t('trigger-key')} />
<Input
hasError={formErrors.triggerKey !== undefined}
type="text"
value={hotKeyForm.triggerKey}
onChange={(e) => handleSetForm('triggerKey', e.target.value)}
/>
{formErrors.triggerKey ? (
<div className="mt-1">
<InlineNotification
type="error"
desc={formErrors.triggerKey}
hideBorder
hidePadding
/>
</div>
) : null}
</div>
<div className="mb-4">
<Label text={t('order-side')} />
<ButtonGroup
activeValue={orderSide}
onChange={(side) => setOrderSide(side)}
activeValue={hotKeyForm.side}
onChange={(side) => handleSetForm('side', side)}
values={['buy', 'sell']}
/>
</div>
<div className="mb-4">
<Label text={t('order-type')} />
<ButtonGroup
activeValue={orderType}
onChange={(type) => setOrderType(type)}
activeValue={hotKeyForm.orderType}
onChange={(type) => handleSetForm('orderType', type)}
values={['limit', 'market']}
/>
</div>
<div className="mb-4">
<Label text={t('order-size-type')} />
<ButtonGroup
activeValue={orderSizeType}
onChange={(type) => setOrderSizeType(type)}
activeValue={hotKeyForm.sizeType}
onChange={(type) => handleSetForm('sizeType', type)}
values={['percentage', 'notional']}
/>
</div>
<div>
<Label text={t('size')} />
<Input
type="text"
value={orderSize}
onChange={(e) => setOrderSize(e.target.value)}
suffix={orderSizeType === 'percentage' ? '%' : 'USD'}
/>
</div>
{orderType === 'limit' ? (
<div className="mt-4">
<Tooltip content="Set a price as a percentage change from the oracle price">
<Label className="tooltip-underline" text={t('price')} />
</Tooltip>
<div className="flex items-start space-x-4">
<div className="w-full">
<Label text={t('size')} />
<Input
hasError={formErrors.size !== undefined}
type="text"
value={orderPrice}
onChange={(e) => setOrderPrice(e.target.value)}
placeholder="e.g. 1%"
suffix="%"
value={hotKeyForm.size}
onChange={(e) => handleSetForm('size', e.target.value)}
suffix={hotKeyForm.sizeType === 'percentage' ? '%' : 'USD'}
/>
{formErrors.size ? (
<div className="mt-1">
<InlineNotification
type="error"
desc={formErrors.size}
hideBorder
hidePadding
/>
</div>
) : null}
</div>
) : null}
{hotKeyForm.orderType === 'limit' ? (
<div className="w-full">
<Tooltip content="Set a price as a percentage change from the oracle price">
<Label className="tooltip-underline" text={t('price')} />
</Tooltip>
<Input
hasError={formErrors.price !== undefined}
type="text"
value={hotKeyForm.price}
onChange={(e) => handleSetForm('price', e.target.value)}
placeholder="e.g. 1%"
suffix="%"
/>
{formErrors.price ? (
<div className="mt-1">
<InlineNotification
type="error"
desc={formErrors.price}
hideBorder
hidePadding
/>
</div>
) : null}
</div>
) : null}
</div>
<div className="flex flex-wrap md:flex-nowrap">
{orderType === 'limit' ? (
{hotKeyForm.orderType === 'limit' ? (
<div className="flex">
<div className="mr-3 mt-4" id="trade-step-six">
<Tooltip
@ -221,7 +385,7 @@ const HotKeyModal = ({ isOpen, onClose }: ModalProps) => {
content={t('trade:tooltip-post')}
>
<Checkbox
checked={postOnly}
checked={hotKeyForm.post}
onChange={(e) => handlePostOnlyChange(e.target.checked)}
>
{t('trade:post')}
@ -236,7 +400,7 @@ const HotKeyModal = ({ isOpen, onClose }: ModalProps) => {
>
<div className="flex items-center text-xs text-th-fgd-3">
<Checkbox
checked={ioc}
checked={hotKeyForm.ioc}
onChange={(e) => handleIocChange(e.target.checked)}
>
IOC
@ -253,8 +417,8 @@ const HotKeyModal = ({ isOpen, onClose }: ModalProps) => {
content={t('trade:tooltip-enable-margin')}
>
<Checkbox
checked={margin}
onChange={(e) => setMargin(e.target.checked)}
checked={hotKeyForm.margin}
onChange={(e) => handleSetForm('margin', e.target.checked)}
>
{t('trade:margin')}
</Checkbox>
@ -270,8 +434,8 @@ const HotKeyModal = ({ isOpen, onClose }: ModalProps) => {
>
<div className="flex items-center text-xs text-th-fgd-3">
<Checkbox
checked={reduceOnly}
onChange={(e) => setReduceOnly(e.target.checked)}
checked={hotKeyForm.reduce}
onChange={(e) => handleSetForm('reduce', e.target.checked)}
>
{t('trade:reduce-only')}
</Checkbox>
@ -279,11 +443,7 @@ const HotKeyModal = ({ isOpen, onClose }: ModalProps) => {
</Tooltip>
</div>
</div>
<Button
className="mt-6 w-full"
disabled={disabled}
onClick={handleSave}
>
<Button className="mt-6 w-full" onClick={handleSave}>
{t('save-hot-key')}
</Button>
</>

View File

@ -42,7 +42,7 @@ const NotificationSettings = () => {
<h2 className="text-base">{t('settings:notifications')}</h2>
</div>
{isAuth ? (
<div className="flex items-center justify-between border-t border-th-bkg-3 p-4">
<div className="flex items-center justify-between border-y border-th-bkg-3 p-4">
<p>{t('settings:limit-order-filled')}</p>
<Switch
checked={!!data?.fillsNotifications}
@ -55,7 +55,7 @@ const NotificationSettings = () => {
/>
</div>
) : (
<div className="mb-8 rounded-lg border border-th-bkg-3 p-6">
<div className="rounded-lg border border-th-bkg-3 p-6">
{connected ? (
<div className="flex flex-col items-center">
<BellIcon className="mb-2 h-6 w-6 text-th-fgd-4" />

View File

@ -1,3 +1,4 @@
import { useViewport } from 'hooks/useViewport'
import AnimationSettings from './AnimationSettings'
import DisplaySettings from './DisplaySettings'
import HotKeysSettings from './HotKeysSettings'
@ -5,8 +6,11 @@ import NotificationSettings from './NotificationSettings'
import PreferredExplorerSettings from './PreferredExplorerSettings'
import RpcSettings from './RpcSettings'
import SoundSettings from './SoundSettings'
import { breakpoints } from 'utils/theme'
const SettingsPage = () => {
const { width } = useViewport()
const isMobile = width ? width < breakpoints.lg : false
return (
<div className="grid grid-cols-12">
<div className="col-span-12 border-b border-th-bkg-3 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
@ -15,12 +19,14 @@ const SettingsPage = () => {
<div className="col-span-12 border-b border-th-bkg-3 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
<DisplaySettings />
</div>
<div className="col-span-12 border-b border-th-bkg-3 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
<div className="col-span-12 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
<NotificationSettings />
</div>
<div className="col-span-12 border-b border-th-bkg-3 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
<HotKeysSettings />
</div>
{!isMobile ? (
<div className="col-span-12 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
<HotKeysSettings />
</div>
) : null}
<div className="col-span-12 border-b border-th-bkg-3 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
<AnimationSettings />
</div>

View File

@ -25,6 +25,7 @@ import { useSpotMarketMax } from './SpotSlider'
import useMangoAccount from 'hooks/useMangoAccount'
import { Market } from '@project-serum/serum'
import { useRouter } from 'next/router'
import useUnownedAccount from 'hooks/useUnownedAccount'
const set = mangoStore.getState().set
@ -90,7 +91,8 @@ const TradeHotKeys = ({ children }: { children: ReactNode }) => {
selectedMarket,
serumOrPerpMarket,
} = useSelectedMarket()
const { mangoAccount } = useMangoAccount()
const { mangoAccount, mangoAccountAddress } = useMangoAccount()
const { isUnownedAccount } = useUnownedAccount()
const { asPath } = useRouter()
const [hotKeys] = useLocalStorageState(HOT_KEYS_KEY, [])
const [placingOrder, setPlacingOrder] = useState(false)
@ -139,7 +141,6 @@ const TradeHotKeys = ({ children }: { children: ReactNode }) => {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const mangoAccount = mangoStore.getState().mangoAccount.current
// const tradeForm = mangoStore.getState().tradeForm
const actions = mangoStore.getState().actions
const selectedMarket = mangoStore.getState().selectedMarket.current
const { ioc, orderPrice, orderSide, orderType, postOnly, reduceOnly } =
@ -303,7 +304,13 @@ const TradeHotKeys = ({ children }: { children: ReactNode }) => {
}
}, [placingOrder])
return hotKeys.length && asPath.includes('/trade') ? (
const showHotKeys =
hotKeys.length &&
asPath.includes('/trade') &&
mangoAccountAddress &&
!isUnownedAccount
return showHotKeys ? (
<Hotkeys
keyName={hotKeys.map((k: HotKey) => k.keySequence).toString()}
onKeyDown={onKeyDown}