mango-v4-ui/components/settings/HotKeysSettings.tsx

495 lines
16 KiB
TypeScript
Raw Normal View History

2023-06-20 16:16:58 -07:00
import ButtonGroup from '@components/forms/ButtonGroup'
2023-06-21 17:48:15 -07:00
import Checkbox from '@components/forms/Checkbox'
2023-06-20 16:16:58 -07:00
import Input from '@components/forms/Input'
import Label from '@components/forms/Label'
2023-06-24 05:44:15 -07:00
import Button, { IconButton } from '@components/shared/Button'
2023-06-22 20:37:34 -07:00
import InlineNotification from '@components/shared/InlineNotification'
2023-06-20 16:16:58 -07:00
import Modal from '@components/shared/Modal'
2023-06-22 20:37:34 -07:00
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
2023-06-20 16:16:58 -07:00
import Tooltip from '@components/shared/Tooltip'
2023-06-22 20:37:34 -07:00
import { KeyIcon, TrashIcon } from '@heroicons/react/20/solid'
2023-06-20 16:16:58 -07:00
import useLocalStorageState from 'hooks/useLocalStorageState'
import { useTranslation } from 'next-i18next'
2023-06-22 20:37:34 -07:00
import { useState } from 'react'
2023-06-20 16:16:58 -07:00
import { ModalProps } from 'types/modal'
import { HOT_KEYS_KEY } from 'utils/constants'
2023-06-21 17:48:15 -07:00
export type HotKey = {
ioc: boolean
keySequence: string
margin: boolean
orderSide: 'buy' | 'sell'
orderSizeType: 'percentage' | 'notional'
orderSize: string
orderType: 'limit' | 'market'
orderPrice: string
postOnly: boolean
reduceOnly: boolean
}
2023-06-20 16:16:58 -07:00
const HotKeysSettings = () => {
2023-06-24 05:44:15 -07:00
const { t } = useTranslation(['common', 'settings', 'trade'])
2023-06-22 20:37:34 -07:00
const [hotKeys, setHotKeys] = useLocalStorageState(HOT_KEYS_KEY, [])
2023-06-20 16:16:58 -07:00
const [showHotKeyModal, setShowHotKeyModal] = useState(false)
2023-06-22 20:37:34 -07:00
const handleDeleteKey = (key: string) => {
const newKeys = hotKeys.filter((hk: HotKey) => hk.keySequence !== key)
setHotKeys([...newKeys])
}
2023-06-20 16:16:58 -07:00
return (
<>
2023-06-24 05:44:15 -07:00
<div className="mb-4 flex items-center justify-between">
2023-06-24 06:06:19 -07:00
<div className="pr-6">
2023-06-24 05:44:15 -07:00
<h2 className="mb-1 text-base">{t('settings:hot-keys')}</h2>
<p>{t('settings:hot-keys-desc')}</p>
</div>
2023-06-22 20:37:34 -07:00
{hotKeys.length ? (
2023-06-24 06:06:19 -07:00
<Button
className="whitespace-nowrap"
2023-06-25 18:11:31 -07:00
disabled={hotKeys.length >= 20}
2023-06-24 06:06:19 -07:00
onClick={() => setShowHotKeyModal(true)}
secondary
>
2023-06-24 05:44:15 -07:00
{t('settings:new-hot-key')}
</Button>
2023-06-22 20:37:34 -07:00
) : null}
2023-06-21 17:48:15 -07:00
</div>
2023-06-25 18:11:31 -07:00
{hotKeys.length === 20 ? (
<div className="mb-4">
<InlineNotification
type="warning"
desc={t('settings:error-key-limit-reached')}
/>
</div>
) : null}
2023-06-20 16:16:58 -07:00
{hotKeys.length ? (
2023-06-22 20:37:34 -07:00
<Table>
<thead>
<TrHead>
2023-06-24 05:44:15 -07:00
<Th className="text-left">{t('settings:key-sequence')}</Th>
<Th className="text-right">{t('trade:order-type')}</Th>
<Th className="text-right">{t('trade:side')}</Th>
<Th className="text-right">{t('trade:size')}</Th>
2023-06-22 20:37:34 -07:00
<Th className="text-right">{t('price')}</Th>
2023-06-24 05:44:15 -07:00
<Th className="text-right">{t('settings:options')}</Th>
2023-06-22 20:37:34 -07:00
<Th />
</TrHead>
</thead>
<tbody>
{hotKeys.map((hk: HotKey) => {
const {
keySequence,
orderSide,
orderPrice,
orderSize,
orderSizeType,
orderType,
ioc,
margin,
reduceOnly,
postOnly,
} = hk
const size =
orderSizeType === 'percentage'
2023-06-24 05:44:15 -07:00
? t('settings:percentage-of-max', { size: orderSize })
2023-06-22 20:37:34 -07:00
: `$${orderSize}`
2023-06-24 05:44:15 -07:00
const price = orderPrice
? `${orderPrice}% ${
orderSide === 'buy'
? t('settings:below')
: t('settings:above')
} oracle`
: t('trade:market')
2023-06-22 20:37:34 -07:00
const options = {
margin: margin,
IOC: ioc,
post: postOnly,
reduce: reduceOnly,
}
return (
<TrBody key={keySequence} className="text-right">
<Td className="text-left">{keySequence}</Td>
2023-06-24 05:44:15 -07:00
<Td className="text-right">{t(`trade:${orderType}`)}</Td>
<Td className="text-right">{t(orderSide)}</Td>
2023-06-22 20:37:34 -07:00
<Td className="text-right">{size}</Td>
<Td className="text-right">{price}</Td>
<Td className="text-right">
{Object.entries(options).map((e) => {
return e[1]
2023-06-24 05:44:15 -07:00
? `${e[0] !== 'margin' ? ', ' : ''}${t(
2023-07-21 11:47:53 -07:00
`trade:${e[0]}`,
2023-06-24 05:44:15 -07:00
)}`
2023-06-22 20:37:34 -07:00
: ''
})}
</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>
2023-06-20 16:16:58 -07:00
) : (
2023-06-22 20:37:34 -07:00
<div className="rounded-lg border border-th-bkg-3 p-6">
2023-06-20 16:16:58 -07:00
<div className="flex flex-col items-center">
<KeyIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
2023-06-24 05:44:15 -07:00
<p className="mb-4">{t('settings:no-hot-keys')}</p>
2023-06-20 16:16:58 -07:00
<Button onClick={() => setShowHotKeyModal(true)}>
2023-06-24 05:44:15 -07:00
<div className="flex items-center">
{t('settings:new-hot-key')}
</div>
2023-06-20 16:16:58 -07:00
</Button>
</div>
</div>
)}
{showHotKeyModal ? (
<HotKeyModal
isOpen={showHotKeyModal}
onClose={() => setShowHotKeyModal(false)}
/>
) : null}
</>
)
}
export default HotKeysSettings
2023-06-22 20:37:34 -07:00
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,
}
2023-06-20 16:16:58 -07:00
const HotKeyModal = ({ isOpen, onClose }: ModalProps) => {
2023-06-24 05:44:15 -07:00
const { t } = useTranslation(['common', 'settings', 'trade'])
2023-06-21 17:48:15 -07:00
const [hotKeys, setHotKeys] = useLocalStorageState<HotKey[]>(HOT_KEYS_KEY, [])
2023-06-22 20:37:34 -07:00
const [hotKeyForm, setHotKeyForm] = useState<HotKeyForm>({
...DEFAULT_FORM_VALUES,
})
const [formErrors, setFormErrors] = useState<FormErrors>({})
2023-06-21 17:48:15 -07:00
2023-06-22 20:37:34 -07:00
const handleSetForm = (propertyName: string, value: string | boolean) => {
setFormErrors({})
setHotKeyForm((prevState) => ({ ...prevState, [propertyName]: value }))
}
2023-06-21 17:48:15 -07:00
2023-06-22 20:37:34 -07:00
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) {
2023-06-24 05:44:15 -07:00
invalidFields[key] = t('settings:error-too-many-characters')
2023-06-21 17:48:15 -07:00
}
2023-06-22 20:37:34 -07:00
if (!alphanumericRegex.test(value)) {
2023-06-24 05:44:15 -07:00
invalidFields[key] = t('settings:error-alphanumeric-only')
2023-06-22 20:37:34 -07:00
}
}
for (const key of requiredFields) {
const value = form[key] as string
if (!value) {
if (hotKeyForm.orderType === 'market') {
if (key !== 'price') {
2023-06-24 05:44:15 -07:00
invalidFields[key] = t('settings:error-required-field')
2023-06-22 20:37:34 -07:00
}
} else {
2023-06-24 05:44:15 -07:00
invalidFields[key] = t('settings:error-required-field')
2023-06-22 20:37:34 -07:00
}
}
}
for (const key of numberFields) {
const value = form[key] as string
if (value) {
if (isNaN(parseFloat(value))) {
2023-06-24 05:44:15 -07:00
invalidFields[key] = t('settings:error-must-be-number')
2023-06-22 20:37:34 -07:00
}
if (parseFloat(value) < 0) {
2023-06-24 05:44:15 -07:00
invalidFields[key] = t('settings:error-must-be-above-zero')
2023-06-22 20:37:34 -07:00
}
if (parseFloat(value) > 100) {
if (key === 'price') {
2023-06-24 05:44:15 -07:00
invalidFields[key] = t('settings:error-must-be-below-100')
2023-06-22 20:37:34 -07:00
} else {
if (hotKeyForm.sizeType === 'percentage') {
2023-06-24 05:44:15 -07:00
invalidFields[key] = t('settings:error-must-be-below-100')
2023-06-22 20:37:34 -07:00
}
}
}
}
}
2023-06-25 18:02:52 -07:00
const newKeySequence = `${form.baseKey}+${form.triggerKey}`
const keyExists = hotKeys.find((k) => k.keySequence === newKeySequence)
if (keyExists) {
invalidFields.triggerKey = t('settings:error-key-in-use')
}
2023-06-22 20:37:34 -07:00
if (Object.keys(invalidFields).length) {
setFormErrors(invalidFields)
}
return invalidFields
}
2023-06-21 17:48:15 -07:00
const handleSave = () => {
2023-06-22 20:37:34 -07:00
const invalidFields = isFormValid(hotKeyForm)
if (Object.keys(invalidFields).length) {
return
}
2023-06-21 17:48:15 -07:00
const newHotKey = {
2023-06-22 20:37:34 -07:00
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,
2023-06-21 17:48:15 -07:00
}
setHotKeys([...hotKeys, newHotKey])
onClose()
}
2023-06-20 16:16:58 -07:00
return (
<Modal isOpen={isOpen} onClose={onClose}>
<>
2023-06-24 05:44:15 -07:00
<h2 className="mb-4 text-center">{t('settings:new-hot-key')}</h2>
2023-06-20 16:16:58 -07:00
<div className="mb-4">
2023-06-24 05:44:15 -07:00
<Label text={t('settings:base-key')} />
2023-06-22 20:37:34 -07:00
<ButtonGroup
activeValue={hotKeyForm.baseKey}
onChange={(key) => handleSetForm('baseKey', key)}
values={['shift', 'ctrl', 'option']}
/>
</div>
<div className="mb-4">
2023-06-24 05:44:15 -07:00
<Label text={t('settings:trigger-key')} />
2023-06-20 16:16:58 -07:00
<Input
2023-06-22 20:37:34 -07:00
hasError={formErrors.triggerKey !== undefined}
2023-06-20 16:16:58 -07:00
type="text"
2023-06-22 20:37:34 -07:00
value={hotKeyForm.triggerKey}
2023-06-25 18:14:28 -07:00
onChange={(e) =>
handleSetForm('triggerKey', e.target.value.toLowerCase())
}
2023-06-20 16:16:58 -07:00
/>
2023-06-22 20:37:34 -07:00
{formErrors.triggerKey ? (
<div className="mt-1">
<InlineNotification
type="error"
desc={formErrors.triggerKey}
hideBorder
hidePadding
/>
</div>
) : null}
2023-06-20 16:16:58 -07:00
</div>
<div className="mb-4">
2023-06-24 05:44:15 -07:00
<Label text={t('settings:order-side')} />
2023-06-20 16:16:58 -07:00
<ButtonGroup
2023-06-22 20:37:34 -07:00
activeValue={hotKeyForm.side}
2023-06-24 05:44:15 -07:00
names={[t('buy'), t('sell')]}
2023-06-22 20:37:34 -07:00
onChange={(side) => handleSetForm('side', side)}
2023-06-21 17:48:15 -07:00
values={['buy', 'sell']}
2023-06-20 16:16:58 -07:00
/>
</div>
<div className="mb-4">
2023-06-24 05:44:15 -07:00
<Label text={t('trade:order-type')} />
2023-06-20 16:16:58 -07:00
<ButtonGroup
2023-06-22 20:37:34 -07:00
activeValue={hotKeyForm.orderType}
2023-06-24 05:44:15 -07:00
names={[t('trade:limit'), t('market')]}
2023-06-22 20:37:34 -07:00
onChange={(type) => handleSetForm('orderType', type)}
2023-06-20 16:16:58 -07:00
values={['limit', 'market']}
/>
</div>
<div className="mb-4">
2023-06-24 05:44:15 -07:00
<Label text={t('settings:order-size-type')} />
2023-06-20 16:16:58 -07:00
<ButtonGroup
2023-06-22 20:37:34 -07:00
activeValue={hotKeyForm.sizeType}
2023-06-24 05:44:15 -07:00
names={[t('settings:percentage'), t('settings:notional')]}
2023-06-22 20:37:34 -07:00
onChange={(type) => handleSetForm('sizeType', type)}
2023-06-20 16:16:58 -07:00
values={['percentage', 'notional']}
/>
</div>
2023-06-22 20:37:34 -07:00
<div className="flex items-start space-x-4">
<div className="w-full">
2023-06-24 05:44:15 -07:00
<Tooltip
content={
hotKeyForm.sizeType === 'notional'
? t('settings:tooltip-hot-key-notional-size')
: t('settings:tooltip-hot-key-percentage-size')
}
>
<Label className="tooltip-underline" text={t('trade:size')} />
</Tooltip>
2023-06-20 16:16:58 -07:00
<Input
2023-06-22 20:37:34 -07:00
hasError={formErrors.size !== undefined}
2023-06-20 16:16:58 -07:00
type="text"
2023-06-22 20:37:34 -07:00
value={hotKeyForm.size}
onChange={(e) => handleSetForm('size', e.target.value)}
suffix={hotKeyForm.sizeType === 'percentage' ? '%' : 'USD'}
2023-06-20 16:16:58 -07:00
/>
2023-06-22 20:37:34 -07:00
{formErrors.size ? (
<div className="mt-1">
<InlineNotification
type="error"
desc={formErrors.size}
hideBorder
hidePadding
/>
</div>
) : null}
2023-06-20 16:16:58 -07:00
</div>
2023-06-22 20:37:34 -07:00
{hotKeyForm.orderType === 'limit' ? (
<div className="w-full">
2023-06-24 05:44:15 -07:00
<Tooltip content={t('settings:tooltip-hot-key-price')}>
2023-06-22 20:37:34 -07:00
<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>
2023-06-21 17:48:15 -07:00
<div className="flex flex-wrap md:flex-nowrap">
2023-06-22 20:37:34 -07:00
{hotKeyForm.orderType === 'limit' ? (
2023-06-21 17:48:15 -07:00
<div className="flex">
<div className="mr-3 mt-4" id="trade-step-six">
<Tooltip
className="hidden md:block"
delay={100}
content={t('trade:tooltip-post')}
>
<Checkbox
2023-06-22 20:37:34 -07:00
checked={hotKeyForm.post}
2023-06-21 17:48:15 -07:00
onChange={(e) => handlePostOnlyChange(e.target.checked)}
>
{t('trade:post')}
</Checkbox>
</Tooltip>
</div>
<div className="mr-3 mt-4" id="trade-step-seven">
<Tooltip
className="hidden md:block"
delay={100}
content={t('trade:tooltip-ioc')}
>
<div className="flex items-center text-xs text-th-fgd-3">
<Checkbox
2023-06-22 20:37:34 -07:00
checked={hotKeyForm.ioc}
2023-06-21 17:48:15 -07:00
onChange={(e) => handleIocChange(e.target.checked)}
>
IOC
</Checkbox>
</div>
</Tooltip>
</div>
</div>
) : null}
<div className="mr-3 mt-4" id="trade-step-eight">
2023-06-21 17:48:15 -07:00
<Tooltip
className="hidden md:block"
delay={100}
content={t('trade:tooltip-enable-margin')}
>
<Checkbox
2023-06-22 20:37:34 -07:00
checked={hotKeyForm.margin}
onChange={(e) => handleSetForm('margin', e.target.checked)}
2023-06-21 17:48:15 -07:00
>
{t('trade:margin')}
</Checkbox>
</Tooltip>
</div>
<div className="mr-3 mt-4">
<Tooltip
className="hidden md:block"
delay={100}
content={
'Reduce will only decrease the size of an open position. This is often used for closing a position.'
}
>
<div className="flex items-center text-xs text-th-fgd-3">
<Checkbox
2023-06-22 20:37:34 -07:00
checked={hotKeyForm.reduce}
onChange={(e) => handleSetForm('reduce', e.target.checked)}
2023-06-21 17:48:15 -07:00
>
{t('trade:reduce-only')}
</Checkbox>
</div>
</Tooltip>
</div>
</div>
2023-06-22 20:37:34 -07:00
<Button className="mt-6 w-full" onClick={handleSave}>
2023-06-24 05:44:15 -07:00
{t('settings:save-hot-key')}
2023-06-21 17:48:15 -07:00
</Button>
2023-06-20 16:16:58 -07:00
</>
</Modal>
)
}