Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Buy sell panel slider #568

Merged
merged 40 commits into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
370fc0e
Include the slider to PercentageSlider
saidam90 Aug 13, 2024
34e602b
Fix the range step and css
saidam90 Aug 16, 2024
6cf9bd1
Add trigger for Market Sell function
saidam90 Aug 24, 2024
712e3cb
Add trigger for Market Buy function and set the amount in input field
saidam90 Aug 25, 2024
9591871
Fix issue with percentage color filling
saidam90 Aug 25, 2024
25c4445
Reset the input when tabs are switched
saidam90 Aug 25, 2024
16fc0fd
Fix Limit Sell orders
saidam90 Aug 25, 2024
921fca0
Delete outcommented code
saidam90 Aug 25, 2024
3037151
Merge branch 'main' into buy-sell-panel-slider
saidam90 Aug 25, 2024
3ed04a6
Delete tippy.js
saidam90 Aug 28, 2024
ae8518e
Simplify slider, reduce number of props
saidam90 Aug 28, 2024
ebb2721
Sync the slider track filling with the input percentage
saidam90 Sep 4, 2024
a913482
Reset the thumb when tabs are switched
saidam90 Sep 4, 2024
5f30520
Fix useEffect function
saidam90 Sep 4, 2024
690fa95
Remove comments
saidam90 Sep 4, 2024
bce4bcf
Merge branch 'main' into buy-sell-panel-slider
saidam90 Sep 4, 2024
f3f5dc2
Merge remote-tracking branch 'origin/buy-sell-panel-slider' into buy-…
saidam90 Sep 4, 2024
67c6f58
Fix slider to sync only with one field in Limit orders
saidam90 Sep 5, 2024
e1a4e65
Refactor sliderCallback function
saidam90 Sep 5, 2024
78c26a8
Subtract xrd fee when the slider is used
saidam90 Sep 5, 2024
f175e67
Add dependencies in sliderCallback function
saidam90 Sep 5, 2024
454e484
Fix lint error
saidam90 Sep 5, 2024
7dc0d40
Remove dependencies from the callBack function
saidam90 Sep 5, 2024
db5878c
Fix bug in the callBack function
saidam90 Sep 5, 2024
d08631a
Merge branch 'main' into buy-sell-panel-slider
saidam90 Sep 8, 2024
8ff692a
Make css inline where possible
saidam90 Sep 9, 2024
356867a
Replace calculations with Calculator class
saidam90 Sep 11, 2024
df24521
Delete comments
saidam90 Sep 11, 2024
6b55baa
Merge remote-tracking branch 'origin/buy-sell-panel-slider' into buy-…
saidam90 Sep 11, 2024
37b6816
Merge branch 'main' into buy-sell-panel-slider
dcts Sep 19, 2024
04dcee3
fix linter errors
dcts Sep 20, 2024
66da348
remove NONZERO_AMOUNT error, and disable button if nonzero amount is …
dcts Sep 20, 2024
842c772
move slider below input field for MARKET orders
dcts Sep 20, 2024
11396f5
reduce emphasis on the slider by adding opacity 70%
dcts Sep 20, 2024
1326a81
add possibility to click labels to set the percentage
dcts Sep 20, 2024
188535a
Merge pull request #584 from DeXter-on-Radix/slider-linter-error-fix
saidam90 Sep 23, 2024
851bd0c
Merge branch 'main' into buy-sell-panel-slider
dcts Nov 1, 2024
19b2931
Merge branch 'main' into buy-sell-panel-slider
dcts Nov 8, 2024
12f2e97
removed zero token1 and token2 validation, as setting the slider UI t…
dcts Nov 8, 2024
43a7548
Merge pull request #601 from DeXter-on-Radix/fix-buy-sell-panel-slide…
dcts Nov 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
280 changes: 274 additions & 6 deletions src/app/components/OrderInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { useEffect, useState, ChangeEvent } from "react";
import { useEffect, useState, useRef, ChangeEvent, useCallback } from "react";
import { AiOutlineInfoCircle } from "react-icons/ai";
import Tippy from "@tippyjs/react";
import "tippy.js/dist/tippy.css";
import "tippy.js/dist/svg-arrow.css";

import {
getPrecision,
Expand Down Expand Up @@ -361,6 +364,7 @@ function SubmitButton() {
side,
type,
token1,
token2,
quote,
quoteDescription,
quoteError,
Expand All @@ -373,7 +377,9 @@ function SubmitButton() {
const hasQuoteError = quoteError !== undefined;
const isLimitOrder = type === OrderType.LIMIT;
const isBuyOrder = side === OrderSide.BUY;
const isZeroAmount = token1.amount === 0 || token2.amount === 0;
const disabled =
isZeroAmount ||
!hasQuote ||
hasQuoteError ||
!isConnected ||
Expand Down Expand Up @@ -442,13 +448,71 @@ function SubmitButton() {
}

function UserInputContainer() {
const { side, type } = useAppSelector((state) => state.orderInput);
const dispatch = useAppDispatch();
const { side, type, token1, token2 } = useAppSelector(
(state) => state.orderInput
);
const balanceToken1 =
useAppSelector((state) => selectBalanceByAddress(state, token1.address)) ||
0;
const balanceToken2 =
useAppSelector((state) => selectBalanceByAddress(state, token2.address)) ||
0;
const bestBuy = useAppSelector((state) => state.orderBook.bestBuy) || 0;
const bestSell = useAppSelector((state) => state.orderBook.bestSell) || 0;

const isMarketOrder = type === "MARKET";
const isLimitOrder = type === "LIMIT";
const isBuyOrder = side === "BUY";
const isSellOrder = side === "SELL";

const sliderCallback = useCallback(
(newPercentage: number) => {
const isXRDToken = isBuyOrder
? token2.symbol === "XRD"
: token1.symbol === "XRD";
let balance = isBuyOrder ? balanceToken2 : balanceToken1;

if (newPercentage === 100 && isXRDToken) {
balance = Math.max(balance - XRD_FEE_ALLOWANCE, 0);
}

const amount = Calculator.divide(
Calculator.multiply(balance, newPercentage),
100
);

const specifiedToken = isBuyOrder
? SpecifiedToken.TOKEN_2
: SpecifiedToken.TOKEN_1;

dispatch(
orderInputSlice.actions.setTokenAmount({
amount,
bestBuy,
bestSell,
balanceToken1,
balanceToken2,
specifiedToken,
})
);
},
[
isBuyOrder,
token1.symbol,
token2.symbol,
balanceToken1,
balanceToken2,
bestBuy,
bestSell,
dispatch,
]
);

useEffect(() => {
sliderCallback(0);
}, [isBuyOrder, isSellOrder, isMarketOrder, isLimitOrder, sliderCallback]);

return (
<div className="bg-base-100 px-5 pb-5 rounded-b">
{isMarketOrder && (
Expand All @@ -457,20 +521,36 @@ function UserInputContainer() {
userAction={UserAction.UPDATE_PRICE}
disabled={true}
/>
<PercentageSlider />
{isSellOrder && ( // specify "Quantity"
<CurrencyInputGroup userAction={UserAction.SET_TOKEN_1} />
)}
{isBuyOrder && ( // specify "Total"
<CurrencyInputGroup userAction={UserAction.SET_TOKEN_2} />
)}
<PercentageSlider
initialPercentage={0}
callbackOnPercentageUpdate={(newPercentage) =>
sliderCallback(newPercentage)
}
isLimitOrder={isLimitOrder}
isBuyOrder={isBuyOrder}
isSellOrder={isSellOrder}
/>
</>
)}
{isLimitOrder && (
<>
<CurrencyInputGroup userAction={UserAction.UPDATE_PRICE} />
<CurrencyInputGroup userAction={UserAction.SET_TOKEN_1} />
<PercentageSlider />
<PercentageSlider
saidam90 marked this conversation as resolved.
Show resolved Hide resolved
initialPercentage={0}
callbackOnPercentageUpdate={(newPercentage) =>
sliderCallback(newPercentage)
}
isLimitOrder={isLimitOrder}
isBuyOrder={isBuyOrder}
isSellOrder={isSellOrder}
/>
<CurrencyInputGroup userAction={UserAction.SET_TOKEN_2} />
{isLimitOrder && <PostOnlyCheckbox />}
</>
Expand All @@ -487,6 +567,7 @@ function CurrencyInputGroupSettings(
): CurrencyInputGroupConfig {
const t = useTranslations();
const dispatch = useAppDispatch();

const {
side,
type,
Expand Down Expand Up @@ -796,10 +877,197 @@ function InputTooltip({ message }: { message: string }) {
}

// TODO(dcts): implement percentage slider in future PR
function PercentageSlider() {
return <></>;
interface PercentageSliderProps {
initialPercentage: number;
callbackOnPercentageUpdate: (newPercentage: number) => void;
isLimitOrder: boolean;
isBuyOrder: boolean;
isSellOrder: boolean;
}
dcts marked this conversation as resolved.
Show resolved Hide resolved

const PercentageSlider: React.FC<PercentageSliderProps> = ({
initialPercentage,
callbackOnPercentageUpdate,
isLimitOrder,
isBuyOrder,
isSellOrder,
}) => {
const [percentage, setPercentage] = useState(initialPercentage);
const [toolTipVisible, setToolTipVisible] = useState(false);
const sliderRef = useRef<HTMLInputElement>(null);
const { token1, token2 } = useAppSelector((state) => state.orderInput);

const inputToken1 = useAppSelector((state) => state.orderInput.token1.amount);
const inputToken2 = useAppSelector((state) => state.orderInput.token2.amount);

const balanceToken1 =
useAppSelector((state) => selectBalanceByAddress(state, token1.address)) ||
0;
const balanceToken2 =
useAppSelector((state) => selectBalanceByAddress(state, token2.address)) ||
0;

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newPercentage = parseInt(e.target.value, 10);
setPercentage(newPercentage);
callbackOnPercentageUpdate(newPercentage);
};

useEffect(() => {
if (!sliderRef.current) {
return;
}

sliderRef.current.value = "0";
sliderRef.current.style.backgroundSize = `0% 100%`;
setPercentage(0);

if (inputToken1 > 0 && isLimitOrder && isBuyOrder) {
return;
} else if (inputToken2 > 0 && isLimitOrder && isSellOrder) {
return;
} else if (balanceToken2 && inputToken2 > 0) {
const newPercentage = Calculator.multiply(
Calculator.divide(inputToken2, balanceToken2),
100
);
setPercentage(newPercentage);
sliderRef.current.style.backgroundSize = `${newPercentage}% 100%`;
} else if (balanceToken1 && inputToken1 > 0) {
const newPercentage = Calculator.multiply(
Calculator.divide(inputToken1, balanceToken1),
100
);
setPercentage(newPercentage);
sliderRef.current.style.backgroundSize = `${newPercentage}% 100%`;
}
}, [
inputToken1,
balanceToken1,
inputToken2,
balanceToken2,
isLimitOrder,
isBuyOrder,
isSellOrder,
]);

const handleClickOnLabel = (newPercentage: number) => {
setPercentage(newPercentage);
callbackOnPercentageUpdate(newPercentage);
};

return (
<>
<div className="slider-container rounded-md w-full relative mt-5 opacity-70">
<div className="absolute w-full">
<input
type="range"
min="0"
max="100"
onChange={handleChange}
value={percentage}
step="1"
ref={sliderRef}
id="range"
className="w-full absolute cursor-pointer text-base appearance-none h-[7px] rounded-md"
style={{
background:
typeof document !== "undefined" && document.dir === "rtl"
? "#474d52"
: "transparent",
backgroundImage:
typeof document !== "undefined" && document.dir === "rtl"
? "linear-gradient(#fff, #fff)"
: "linear-gradient(#474d52, #474d52)",
backgroundSize: `${percentage}% 100%`,
backgroundRepeat: "no-repeat",
}}
onMouseDown={() => setToolTipVisible(true)}
onMouseUp={() => setToolTipVisible(false)}
onMouseEnter={() => setToolTipVisible(true)}
onMouseLeave={() => setToolTipVisible(false)}
/>
<Tippy
content={<span>{Math.round(percentage)}%</span>}
visible={toolTipVisible}
onClickOutside={() => setToolTipVisible(false)}
arrow={false}
theme="custom"
placement="top"
>
<div
className="relative"
style={{
left: `${percentage}%`,
transform: "translateX(-50%)",
}}
></div>
</Tippy>
</div>

<div className="slider-track relative cursor-pointer">
<div className="flex justify-between items-center">
{Array(5)
.fill(0)
.map((_, index) => (
<span
key={index}
className="dot h-[7px] w-[7px] bg-white rounded-full z-[1] cursor-pointer"
style={
{
left: `Calculator.divide((Calculator.multiply(index, 100)), 5)}%`,
} as React.CSSProperties
}
></span>
))}
</div>
</div>
<div className="w-full">
<div className="slider-labels">
<div className="flex justify-between text-xxs mt-1 mb-5">
<span
className="absolute select-none"
style={{ left: "0%" }}
onClick={() => handleClickOnLabel(0)}
>
0%
</span>
<span
className="absolute select-none cursor-pointer"
style={{ left: "25%", transform: "translateX(-50%)" }}
onClick={() => handleClickOnLabel(25)}
>
25%
</span>
<span
className="absolute select-none cursor-pointer"
style={{ left: "50%", transform: "translateX(-50%)" }}
onClick={() => handleClickOnLabel(50)}
>
50%
</span>
<span
className="absolute select-none cursor-pointer"
style={{ left: "75%", transform: "translateX(-50%)" }}
onClick={() => handleClickOnLabel(75)}
>
75%
</span>
<span
className="absolute select-none cursor-pointer"
style={{ left: "100%", transform: "translateX(-100%)" }}
onClick={() => handleClickOnLabel(100)}
>
100%
</span>
</div>
</div>
</div>
</div>
</>
);
};

// Mimics IMask with improved onAccept, triggered only by user input to avoid rerender bugs.
function CustomNumericIMask({
value,
Expand Down
1 change: 0 additions & 1 deletion src/app/state/locales/en/errors.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"UNSPECIFIED_PRICE": "Price must be specified",
"NONZERO_PRICE": "Price must be greater than 0",
"NONZERO_AMOUNT": "Amount must be greater than 0",
"HIGH_PRICE": "Price is significantly higher than best sell",
"LOW_PRICE": "Price is significantly lower than best buy",
"EXCESSIVE_DECIMALS": "Too many decimal places",
Expand Down
1 change: 0 additions & 1 deletion src/app/state/locales/pt/errors.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"UNSPECIFIED_PRICE": "Preço deve ser especificado",
"NONZERO_PRICE": "Preço deve ser maior que 0",
"NONZERO_AMOUNT": "Quantidade deve ser maior que 0",
"HIGH_PRICE": "Preço está significativamente maior que a melhor oferta de venda",
"LOW_PRICE": "Preço está significativamente menor que a melhor oferta de compra",
"EXCESSIVE_DECIMALS": "Excesso de casas decimais",
Expand Down
26 changes: 0 additions & 26 deletions src/app/state/orderInputSlice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,32 +66,6 @@ describe("OrderInputSlice", () => {
);
});

it("Validation works for zero token1 amount", () => {
store.dispatch(
orderInputSlice.actions.setTokenAmount({
amount: 0,
specifiedToken: SpecifiedToken.TOKEN_1,
})
);
expect(store.getState().orderInput.validationToken1.valid).toBe(false);
expect(store.getState().orderInput.validationToken1.message).toBe(
ErrorMessage.NONZERO_AMOUNT
);
});

it("Validation works for zero token2 amount", () => {
store.dispatch(
orderInputSlice.actions.setTokenAmount({
amount: 0,
specifiedToken: SpecifiedToken.TOKEN_2,
})
);
expect(store.getState().orderInput.validationToken2.valid).toBe(false);
expect(store.getState().orderInput.validationToken2.message).toBe(
ErrorMessage.NONZERO_AMOUNT
);
});

it("Validation works for insufficient balance for token1 on sell order ", () => {
store.dispatch(orderInputSlice.actions.setSide(OrderSide.SELL));
store.dispatch(
Expand Down
Loading
Loading