diff --git a/src/components/default/GenericTable/index.tsx b/src/components/default/GenericTable/index.tsx index 866236c..a4f5123 100644 --- a/src/components/default/GenericTable/index.tsx +++ b/src/components/default/GenericTable/index.tsx @@ -19,8 +19,8 @@ export default function GenericTable(props: GenericTableProps) { renderGroupHeader, sortGroups, responsive, + scrollRef, } = props; - const isMatch = useMediaQuery(responsive?.query ?? ''); const mediaActive = !!responsive?.query && isMatch; @@ -34,7 +34,7 @@ export default function GenericTable(props: GenericTableProps) { if (mediaActive && responsive?.alignOverride) { cols = cols.map((c) => { - const ov = responsive.alignOverride![c.key]; + const ov = responsive.alignOverride?.[c.key]; return ov ? { ...c, align: ov } : c; }); } @@ -60,7 +60,11 @@ export default function GenericTable(props: GenericTableProps) { return (
{data.length > 0 ? ( -
+
= { alignOverride?: Record; tableLayout?: 'auto' | 'fixed'; }; + scrollRef?: React.RefObject; }; diff --git a/src/components/dex/InputPanelItem/index.tsx b/src/components/dex/InputPanelItem/index.tsx index 58c2014..40b92bb 100644 --- a/src/components/dex/InputPanelItem/index.tsx +++ b/src/components/dex/InputPanelItem/index.tsx @@ -69,12 +69,14 @@ function InputPanelItem(props: InputPanelItemProps) { async function postOrder() { const price = new Decimal(priceState); const amount = new Decimal(amountState); + const total = new Decimal(totalState); const isFull = price.greaterThan(0) && price.lessThan(1000000000) && amount.greaterThan(0) && - amount.lessThan(1000000000); + amount.lessThan(1000000000) && + total.greaterThan(0); if (!isFull) return; @@ -123,6 +125,7 @@ function InputPanelItem(props: InputPanelItemProps) { const buttonText = creatingState ? 'Creating...' : 'Create Order'; const isButtonDisabled = !priceValid || !amountValid || !totalValid || creatingState; + const showTotalError = priceState !== '' && amountState !== '' && !totalValid; return (
@@ -213,12 +216,12 @@ function InputPanelItem(props: InputPanelItemProps) {
undefined} currency={secondCurrencyName} label="Total" readonly={true} - invalid={!!totalState && !totalValid} + invalid={showTotalError} />
diff --git a/src/components/dex/OrdersPool/index.tsx b/src/components/dex/OrdersPool/index.tsx index 08670d5..350fcb7 100644 --- a/src/components/dex/OrdersPool/index.tsx +++ b/src/components/dex/OrdersPool/index.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useRef, useState } from 'react'; +import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; import { classes, createOrderSorter, @@ -35,32 +35,92 @@ const tabsData: tabsType[] = [ const OrdersPool = (props: OrdersPoolProps) => { const { ordersBuySell, - OrdersHistory, setOrdersBuySell, currencyNames, ordersLoading, + ordersHistory, filteredOrdersHistory, secondAssetUsdPrice, takeOrderClick, trades, tradesLoading, } = props; - const ordersInfoRef = useRef(null); + const ordersInfoRef = useRef(null); + const scrollRef = useRef(null); + const ordersMiddleRef = useRef(null); const { firstCurrencyName, secondCurrencyName } = currencyNames; const [infoTooltipPos, setInfoTooltipPos] = useState({ x: 0, y: 0 }); const [ordersInfoTooltip, setOrdersInfoTooltip] = useState(null); const [currentOrder, setCurrentOrder] = useState(tabsData[0]); - const { maxBuyLeftValue, maxSellLeftValue } = OrdersHistory.reduce( - (acc, order) => { - const left = parseFloat(String(order.left)) || 0; - if (order.type === 'buy') acc.maxBuyLeftValue = Math.max(acc.maxBuyLeftValue, left); - if (order.type === 'sell') acc.maxSellLeftValue = Math.max(acc.maxSellLeftValue, left); - return acc; - }, - { maxBuyLeftValue: 0, maxSellLeftValue: 0 }, - ); - const totalLeft = maxBuyLeftValue + maxSellLeftValue; + const totals = useMemo(() => { + let buyTotal = new Decimal(0); + let sellTotal = new Decimal(0); + let maxBuyRow = new Decimal(0); + let maxSellRow = new Decimal(0); + + for (const o of ordersHistory) { + const qty = new Decimal(o.amount || 0); + const price = new Decimal(o.price || 0); + const rowTotal = qty.mul(price); + + if (o.type === 'buy') { + buyTotal = buyTotal.plus(rowTotal); + if (rowTotal.gt(maxBuyRow)) maxBuyRow = rowTotal; + } else if (o.type === 'sell') { + sellTotal = sellTotal.plus(rowTotal); + if (rowTotal.gt(maxSellRow)) maxSellRow = rowTotal; + } + } + + const totalZano = buyTotal.plus(sellTotal); + const pct = (part: Decimal, whole: Decimal) => + whole.gt(0) ? part.mul(100).div(whole) : new Decimal(0); + + const buyPct = pct(buyTotal, totalZano); + const sellPct = pct(sellTotal, totalZano); + + return { + buyTotal, + sellTotal, + totalZano, + buyPct, + sellPct, + maxBuyRow, + maxSellRow, + }; + }, [ordersHistory]); + + const toDisplayPair = (buyPctDec: Decimal, sellPctDec: Decimal) => { + const MIN_DISPLAY_PCT = 1; + const buyRaw = buyPctDec.toNumber(); + const sellRaw = sellPctDec.toNumber(); + + if (!Number.isFinite(buyRaw) || !Number.isFinite(sellRaw)) return { buy: 0, sell: 0 }; + + if (buyRaw === 0 && sellRaw === 0) return { buy: 0, sell: 0 }; + if (buyRaw === 0) return { buy: 0, sell: 100 }; + if (sellRaw === 0) return { buy: 100, sell: 0 }; + + let buyDisp = Math.floor(buyRaw); + let sellDisp = Math.floor(sellRaw); + + if (buyDisp < MIN_DISPLAY_PCT) buyDisp = MIN_DISPLAY_PCT; + if (sellDisp < MIN_DISPLAY_PCT) sellDisp = MIN_DISPLAY_PCT; + + const diff = 100 - (buyDisp + sellDisp); + if (diff !== 0) { + if (buyRaw >= sellRaw) buyDisp += diff; + else sellDisp += diff; + } + + buyDisp = Math.max(0, Math.min(100, buyDisp)); + sellDisp = Math.max(0, Math.min(100, sellDisp)); + + return { buy: buyDisp, sell: sellDisp }; + }; + + const { buy: buyDisp, sell: sellDisp } = toDisplayPair(totals.buyPct, totals.sellPct); const moveInfoTooltip = (event: React.MouseEvent) => { setInfoTooltipPos({ x: event.clientX, y: event.clientY }); @@ -84,6 +144,36 @@ const OrdersPool = (props: OrdersPoolProps) => { [firstCurrencyName, secondCurrencyName], ); + useLayoutEffect(() => { + if (!scrollRef.current) return; + + const parent = scrollRef.current; + + if (ordersBuySell.code === 'all' && ordersMiddleRef.current) { + const child = ordersMiddleRef.current; + + const parentRect = parent.getBoundingClientRect(); + const childRect = child.getBoundingClientRect(); + + const scrollTop = + childRect.top - + parentRect.top + + parent.scrollTop - + parent.clientHeight / 2 + + childRect.height / 2; + + parent.scrollTo({ + top: scrollTop, + behavior: 'smooth', + }); + } else { + parent.scrollTo({ + top: 0, + behavior: 'smooth', + }); + } + }, [ordersLoading, filteredOrdersHistory.length, ordersBuySell.code]); + const sortedTrades = createOrderSorter({ getPrice: (e) => e.price, getSide: (e) => e.type, @@ -104,33 +194,42 @@ const OrdersPool = (props: OrdersPoolProps) => { columns={ordersPool} data={filteredOrdersHistory.sort(sortedTrades)} getRowKey={(r) => r.id} - getRowProps={(row) => ({ - className: styles[row.type], - style: { - '--precentage': `${( - (parseFloat(String(row.left)) / - (row.type === 'buy' - ? maxBuyLeftValue - : maxSellLeftValue)) * - 100 - ).toFixed(2)}%`, - } as React.CSSProperties, - onClick: (event) => { - takeOrderClick(event, row); - }, - onMouseMove: (event) => { - const tr = event.target as HTMLElement; - if (tr.classList.contains('alias')) { - setOrdersInfoTooltip(null); - } - }, - onMouseEnter: () => { - setOrdersInfoTooltip(row); - }, - onMouseLeave: () => { - setOrdersInfoTooltip(null); - }, - })} + groupBy={(r) => r.type} + scrollRef={scrollRef} + renderGroupHeader={({ groupKey }) => { + if (groupKey === 'buy') { + return ( +
+ ); + } + }} + getRowProps={(row) => { + const rowTotalZano = new Decimal(row.left || 0).mul( + new Decimal(row.price || 0), + ); + const denom = + row.type === 'buy' + ? totals.maxBuyRow + : totals.maxSellRow; + const widthPct = denom.gt(0) + ? rowTotalZano.mul(100).div(denom) + : new Decimal(0); + + return { + className: styles[row.type], + style: { + '--precentage': `${widthPct.toDecimalPlaces(2).toString()}%`, + } as React.CSSProperties, + onClick: (event) => takeOrderClick(event, row), + onMouseMove: (event) => { + const tr = event.target as HTMLElement; + if (tr.classList.contains('alias')) + setOrdersInfoTooltip(null); + }, + onMouseEnter: () => setOrdersInfoTooltip(row), + onMouseLeave: () => setOrdersInfoTooltip(null), + }; + }} responsive={{ query: '(max-width: 640px)', hiddenKeys: ['total'], @@ -213,63 +312,24 @@ const OrdersPool = (props: OrdersPoolProps) => {
{renderTable()} - {currentOrder.type === 'orders' && - !ordersLoading && - totalLeft > 0 && - (() => { - const buy = new Decimal(maxBuyLeftValue || 0); - const sell = new Decimal(maxSellLeftValue || 0); - const total = new Decimal(totalLeft); + {currentOrder.type === 'orders' && !ordersLoading && totals.totalZano.gt(0) && ( +
+
+
B
+ {buyDisp}% +
- let buyPct = total.gt(0) ? buy.mul(100).div(total) : new Decimal(0); - let sellPct = total.gt(0) ? sell.mul(100).div(total) : new Decimal(0); - - if (buy.isZero() && sell.gt(0)) { - buyPct = new Decimal(0); - sellPct = new Decimal(100); - } else if (sell.isZero() && buy.gt(0)) { - sellPct = new Decimal(0); - buyPct = new Decimal(100); - } - - const clamp = (d: Decimal) => Decimal.max(0, Decimal.min(100, d)); - - const buyPctClamped = clamp(buyPct); - const sellPctClamped = clamp(sellPct); - - const buyLabel = buyPctClamped - .toDecimalPlaces(0, Decimal.ROUND_DOWN) - .toString(); - const sellLabel = sellPctClamped - .toDecimalPlaces(0, Decimal.ROUND_DOWN) - .toString(); - - return ( -
-
-
B
{buyLabel}% -
-
- {sellLabel}%{' '} -
S
-
-
- ); - })()} +
+ {sellDisp}%
S
+
+
+ )}
diff --git a/src/components/dex/OrdersPool/types.ts b/src/components/dex/OrdersPool/types.ts index 8860011..6260c89 100644 --- a/src/components/dex/OrdersPool/types.ts +++ b/src/components/dex/OrdersPool/types.ts @@ -12,7 +12,7 @@ export interface OrdersPoolProps { secondCurrencyName: string; }; ordersLoading: boolean; - OrdersHistory: PageOrderData[]; + ordersHistory: PageOrderData[]; filteredOrdersHistory: PageOrderData[]; trades: Trade[]; tradesLoading: boolean; diff --git a/src/hook/useOrdereForm.ts b/src/hook/useOrdereForm.ts index aaef1a5..774b05d 100644 --- a/src/hook/useOrdereForm.ts +++ b/src/hook/useOrdereForm.ts @@ -10,6 +10,14 @@ interface UseOrderFormParams { assetsRates: Map; } +function clamp12(str: string) { + try { + return new Decimal(str || '0').toDecimalPlaces(12, Decimal.ROUND_DOWN).toString(); + } catch { + return '0'; + } +} + export function useOrderForm({ pairData, balance, @@ -47,7 +55,7 @@ export function useOrderForm({ thisDP: priceDP, totalDP: priceDP, setThisState: setPrice, - setTotalState: setTotal, + setTotalState: (v: string) => setTotal(clamp12(v)), setThisValid: setPriceValid, setTotalValid, }); @@ -61,7 +69,7 @@ export function useOrderForm({ thisDP: amountDP, totalDP: priceDP, setThisState: setAmount, - setTotalState: setTotal, + setTotalState: (v: string) => setTotal(clamp12(v)), setThisValid: setAmountValid, setTotalValid, balance, diff --git a/src/pages/dex/trading/[id].tsx b/src/pages/dex/trading/[id].tsx index 69ef00c..57cb27c 100644 --- a/src/pages/dex/trading/[id].tsx +++ b/src/pages/dex/trading/[id].tsx @@ -159,11 +159,11 @@ function Trading() { ordersBuySell={ordersBuySell} ordersLoading={ordersLoading} filteredOrdersHistory={filteredOrdersHistory} + ordersHistory={ordersHistory} trades={trades} tradesLoading={tradesLoading} setOrdersBuySell={setOrdersBuySell} takeOrderClick={onHandleTakeOrder} - OrdersHistory={ordersHistory} />
diff --git a/src/styles/Dex.module.scss b/src/styles/Dex.module.scss index bbadf08..6478dd5 100644 --- a/src/styles/Dex.module.scss +++ b/src/styles/Dex.module.scss @@ -8,7 +8,7 @@ height: 54px; padding: 0 12px; - > div { + >div { display: flex; align-items: center; gap: 10px; @@ -136,12 +136,12 @@ width: 240px; height: 42px; - > div:nth-child(1) { + >div:nth-child(1) { border-radius: 10px; padding: 11px 20px; - > div { - > div p { + >div { + >div p { font-size: 14px; font-weight: 500; } @@ -160,12 +160,12 @@ } } - > div:nth-child(2) { - > div { - > div { + >div:nth-child(2) { + >div { + >div { padding: 11px 20px; - > div { + >div { p { font-size: 14px; font-weight: 500; @@ -233,7 +233,7 @@ gap: 20px; .input_wrapper { - padding: 13px; + padding-inline: 13px; max-width: 240px; max-height: 42px; display: flex; @@ -244,7 +244,9 @@ border-radius: 10px; .input { - padding: 0; + border-radius: 0; + padding-inline: 0; + padding-block: 13px; max-width: 185px; background-color: transparent; font-size: 14px; @@ -303,7 +305,7 @@ left: 50%; transform: translateX(-50%); - > p { + >p { width: 100%; font-size: 16px; text-wrap: wrap; @@ -321,11 +323,11 @@ } } - > p { + >p { margin-right: 6px; } - > input { + >input { margin-left: 33px; height: 18px; width: 18px; @@ -387,7 +389,7 @@ left: 50%; transform: translateX(-50%); - > p { + >p { width: 100%; font-size: 16px; text-wrap: wrap; @@ -399,14 +401,14 @@ display: block; } - > svg > * { + >svg>* { opacity: 1; } } } } - > .curve__chart { + >.curve__chart { display: block; width: 100%; height: 50px; @@ -417,7 +419,7 @@ align-items: center; gap: 15px; - > div:first-child { + >div:first-child { width: 48px; height: 48px; padding: 10px; @@ -427,13 +429,13 @@ background: var(--icon-bg-color); border-radius: 50%; - > img { + >img { width: auto; height: 100%; } } - > div:last-child { + >div:last-child { height: 48px; display: flex; flex-direction: column; @@ -502,4 +504,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/utils/handleInputChange.ts b/src/utils/handleInputChange.ts index ee32ed3..7a7f35d 100644 --- a/src/utils/handleInputChange.ts +++ b/src/utils/handleInputChange.ts @@ -3,14 +3,15 @@ import Decimal from 'decimal.js'; import { isPositiveFloatStr } from '@/utils/utils'; import { validateTokensInput } from 'shared/utils'; +type SetStr = (_v: string) => void; interface HandleInputChangeParams { inputValue: string; priceOrAmount: 'price' | 'amount'; otherValue: string; thisDP: number; totalDP: number; - setThisState: Dispatch>; - setTotalState: Dispatch>; + setThisState: SetStr; + setTotalState: SetStr; setThisValid: Dispatch>; setTotalValid: Dispatch>; balance?: string | undefined; @@ -68,18 +69,22 @@ export function handleInputChange({ setThisValid(true); if (!thisDecimal.isNaN() && !otherDecimal.isNaN() && otherValue !== '') { - const total = + const rawTotal = priceOrAmount === 'price' ? thisDecimal.mul(otherDecimal) : otherDecimal.mul(thisDecimal); - setTotalState(total.toString()); + const totalClamped = rawTotal.toDecimalPlaces(totalDP, Decimal.ROUND_DOWN); - const totalValid = validateTokensInput(total.toFixed(totalDP), totalDP); - setTotalValid(totalValid.valid); + setTotalState(totalClamped.toString()); + + const fmtOk = validateTokensInput(totalClamped.toFixed(totalDP), totalDP).valid; + const gtZero = totalClamped.gt(0); + setTotalValid(fmtOk && gtZero); if (priceOrAmount === 'amount' && balance && setRangeInputValue) { - const percent = thisDecimal.div(balance).mul(100); + const bal = new Decimal(balance || '0'); + const percent = bal.gt(0) ? thisDecimal.div(bal).mul(100) : new Decimal(0); setRangeInputValue(percent.toFixed()); } } else {