Skip to content

Commit

Permalink
feat: improved action box
Browse files Browse the repository at this point in the history
  • Loading branch information
k0beLeenders committed Dec 8, 2023
1 parent 497c0ff commit c3109e7
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 132 deletions.
256 changes: 156 additions & 100 deletions apps/marginfi-v2-ui/src/components/common/ActionBox/ActionBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useWalletContext } from "~/hooks/useWalletContext";

import { MrgnLabeledSwitch } from "~/components/common/MrgnLabeledSwitch";
import { ActionBoxTokens } from "~/components/common/ActionBox/ActionBoxTokens";
import { LSTDialog, LSTDialogVariants } from "~/components/common/AssetList";

import { Input } from "~/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select";
Expand Down Expand Up @@ -40,6 +41,12 @@ export const ActionBox = () => {
);
const { walletContextState } = useWalletContext();

const [isLoading, setIsLoading] = React.useState(false);
const [isLSTDialogOpen, setIsLSTDialogOpen] = React.useState(false);
const [lstDialogVariant, setLSTDialogVariant] = React.useState<LSTDialogVariants | null>(null);
const [hasLSTDialogShown, setHasLSTDialogShown] = React.useState<LSTDialogVariants[]>([]);
const [lstDialogCallback, setLSTDialogCallback] = React.useState<(() => void) | null>(null);

const [preview, setPreview] = React.useState<{ key: string; value: string }[]>([]);
const [amount, setAmount] = React.useState<number | null>(null);
const amountInputRef = React.useRef<HTMLInputElement>(null);
Expand All @@ -63,7 +70,7 @@ export const ActionBox = () => {
const isInputDisabled = React.useMemo(() => maxAmount === 0 && !showCloseBalance, [maxAmount, showCloseBalance]);
const walletAmount = React.useMemo(
() =>
selectedToken?.info.state.mint.equals(WSOL_MINT)
selectedToken?.info.state.mint?.equals && selectedToken?.info.state.mint?.equals(WSOL_MINT)
? selectedToken?.userInfo.tokenAccount.balance + nativeSolBalance
: selectedToken?.userInfo.tokenAccount.balance,
[selectedToken]
Expand All @@ -78,7 +85,12 @@ export const ActionBox = () => {

React.useEffect(() => {
setAmount(0);
}, [lendingMode, setAmount, selectedToken]);
if (lendingMode === LendingModes.LEND) {
setActionMode(ActionType.Deposit);
} else {
setActionMode(ActionType.Borrow);
}
}, [lendingMode, selectedToken, setAmount, setActionMode]);

React.useEffect(() => {
if (!selectedToken || !amount) {
Expand Down Expand Up @@ -122,6 +134,7 @@ export const ActionBox = () => {
marginfiAccount,
walletContextState,
}: MarginfiActionParams) => {
setIsLoading(true);
await executeLendingAction({
mfiClient,
actionType: currentAction,
Expand All @@ -132,6 +145,7 @@ export const ActionBox = () => {
walletContextState,
});

setIsLoading(false);
setAmount(0);

// -------- Refresh state
Expand Down Expand Up @@ -168,20 +182,46 @@ export const ActionBox = () => {
}, [selectedToken, selectedAccount, fetchMrgnlendState, setIsRefreshingStore]);

const handleLendingAction = React.useCallback(async () => {
// TODO implement LST dialog
if (!actionMode || !selectedToken || !selectedAccount || !amount) {
return;
}

await executeLendingActionCb({
mfiClient,
actionType: actionMode,
bank: selectedToken,
amount: amount,
nativeSolBalance,
marginfiAccount: selectedAccount,
walletContextState,
});
const action = async () => {
executeLendingActionCb({
mfiClient,
actionType: actionMode,
bank: selectedToken,
amount: amount,
nativeSolBalance,
marginfiAccount: selectedAccount,
walletContextState,
});
};

if (
actionMode === ActionType.Deposit &&
(selectedToken.meta.tokenSymbol === "SOL" || selectedToken.meta.tokenSymbol === "stSOL") &&
!hasLSTDialogShown.includes(selectedToken.meta.tokenSymbol as LSTDialogVariants)
) {
setHasLSTDialogShown((prev) => [...prev, selectedToken.meta.tokenSymbol as LSTDialogVariants]);
setLSTDialogVariant(selectedToken.meta.tokenSymbol);
setIsLSTDialogOpen(true);
setLSTDialogCallback(() => action);

return;
}

await action();

if (
actionMode === ActionType.Withdraw &&
(selectedToken.meta.tokenSymbol === "SOL" || selectedToken.meta.tokenSymbol === "stSOL") &&
!hasLSTDialogShown.includes(selectedToken.meta.tokenSymbol as LSTDialogVariants)
) {
setHasLSTDialogShown((prev) => [...prev, selectedToken.meta.tokenSymbol as LSTDialogVariants]);
setLSTDialogVariant(selectedToken.meta.tokenSymbol);
return;
}
}, [
actionMode,
selectedToken,
Expand All @@ -194,101 +234,117 @@ export const ActionBox = () => {
]);

return (
<div className="bg-background p-4 flex flex-col items-center gap-4">
<div className="space-y-6 text-center w-full flex flex-col items-center">
<div className="flex w-[150px] h-[42px]">
<MrgnLabeledSwitch
labelLeft="Lend"
labelRight="Borrow"
checked={lendingMode === LendingModes.BORROW}
onClick={() => {
setLendingMode(lendingMode === LendingModes.LEND ? LendingModes.BORROW : LendingModes.LEND);
}}
/>
</div>
<p className="text-muted-foreground">Supply. Earn interest. Borrow. Repeat.</p>
</div>
<div className="p-6 bg-background-gray text-white w-full max-w-[480px] rounded-xl">
<div className="flex flex-row items-baseline justify-between">
{hasActivePosition ? (
<Select
value={actionMode}
disabled={!hasActivePosition}
onValueChange={(value) => {
setActionMode(value as ActionType);
<>
<div className="bg-background p-4 flex flex-col items-center gap-4">
<div className="space-y-6 text-center w-full flex flex-col items-center">
<div className="flex w-[150px] h-[42px]">
<MrgnLabeledSwitch
labelLeft="Lend"
labelRight="Borrow"
checked={lendingMode === LendingModes.BORROW}
onClick={() => {
setLendingMode(lendingMode === LendingModes.LEND ? LendingModes.BORROW : LendingModes.LEND);
}}
>
<SelectTrigger
className="w-[160px] h-[35px] rounded-[100px] bg-background-gray-light border-none mb-3"
icon={<ChevronDownIcon className="h-5 w-5 opacity-70" />}
/>
</div>
<p className="text-muted-foreground">Supply. Earn interest. Borrow. Repeat.</p>
</div>
<div className="p-6 bg-background-gray text-white w-full max-w-[480px] rounded-xl">
<div className="flex flex-row items-baseline justify-between">
{hasActivePosition ? (
<Select
value={actionMode}
disabled={!hasActivePosition}
onValueChange={(value) => {
setActionMode(value as ActionType);
}}
>
<div className="flex items-center gap-2 text-lg">
<SelectValue className="text-lg" defaultValue={LendingModes.LEND} placeholder="Select pools" />
</div>
</SelectTrigger>
<SelectTrigger
className="w-[160px] h-[35px] rounded-[100px] bg-background-gray-light border-none mb-3"
icon={<ChevronDownIcon className="h-5 w-5 opacity-70" />}
>
<div className="flex items-center gap-2 text-lg">
<SelectValue className="text-lg" defaultValue={LendingModes.LEND} placeholder="Select pools" />
</div>
</SelectTrigger>

{lendingMode === LendingModes.LEND ? (
<SelectContent>
<SelectItem value={ActionType.Deposit}>You supply</SelectItem>
<SelectItem value={ActionType.Withdraw}>You withdraw</SelectItem>
</SelectContent>
) : (
<SelectContent>
<SelectItem value={ActionType.Borrow}>You borrow</SelectItem>
<SelectItem value={ActionType.Repay}>You repay</SelectItem>
</SelectContent>
)}
</Select>
) : (
<p className="text-lg mb-3">You {lendingMode === LendingModes.LEND ? "supply" : "borrow"}</p>
)}
{selectedToken && (
<div className="inline-flex gap-2 items-baseline">
<div className="h-3.5">
<IconWallet size={16} />
</div>
<span className="text-sm font-normal">
{(walletAmount && walletAmount > 0.01 ? numeralFormatter(walletAmount) : "< 0.01").concat(
" ",
selectedToken?.meta.tokenSymbol
{lendingMode === LendingModes.LEND ? (
<SelectContent>
<SelectItem value={ActionType.Deposit}>You supply</SelectItem>
<SelectItem value={ActionType.Withdraw}>You withdraw</SelectItem>
</SelectContent>
) : (
<SelectContent>
<SelectItem value={ActionType.Borrow}>You borrow</SelectItem>
<SelectItem value={ActionType.Repay}>You repay</SelectItem>
</SelectContent>
)}
</span>
<div onClick={() => setAmount(maxAmount)} className="text-base font-bold cursor-pointer">
MAX
</Select>
) : (
<p className="text-lg mb-3">You {lendingMode === LendingModes.LEND ? "supply" : "borrow"}</p>
)}
{selectedToken && (
<div className="inline-flex gap-2 items-baseline">
<div className="h-3.5">
<IconWallet size={16} />
</div>
<span className="text-sm font-normal">
{(walletAmount && walletAmount > 0.01
? numeralFormatter(walletAmount)
: walletAmount == 0
? "0"
: "< 0.01"
).concat(" ", selectedToken?.meta.tokenSymbol)}
</span>
<div onClick={() => setAmount(maxAmount)} className="text-base font-bold cursor-pointer">
MAX
</div>
</div>
</div>
)}
</div>
<div className="bg-background text-3xl rounded-lg flex justify-between items-center p-4 font-medium mb-5">
<ActionBoxTokens currentToken={selectedToken} setCurrentToken={setSelectedToken} />
<Input
type="number"
ref={amountInputRef}
value={amount!}
disabled={isInputDisabled}
onChange={(e) => setAmount(Number(e.target.value))}
placeholder="0"
className="bg-transparent w-full text-right outline-none focus-visible:outline-none focus-visible:ring-0 border-none text-3xl font-medium"
)}
</div>
<div className="bg-background text-3xl rounded-lg flex justify-between items-center p-4 font-medium mb-5">
<ActionBoxTokens currentToken={selectedToken} setCurrentToken={setSelectedToken} />
<Input
type="number"
ref={amountInputRef}
value={amount!}
disabled={isInputDisabled}
onChange={(e) => setAmount(Number(e.target.value))}
placeholder="0"
className="bg-transparent w-full text-right outline-none focus-visible:outline-none focus-visible:ring-0 border-none text-3xl font-medium"
/>
</div>
<ActionBoxActions
amount={amount ?? 0}
maxAmount={maxAmount}
showCloseBalance={showCloseBalance ?? false}
handleAction={() => (showCloseBalance ? handleCloseBalance() : handleLendingAction())}
isLoading={isLoading}
/>
{selectedToken !== null && amount !== null && preview.length > 0 && (
<dl className="grid grid-cols-2 text-muted-foreground gap-y-2 mt-4 text-sm">
{preview.map((item) => (
<React.Fragment key={item.key}>
<dt>{item.key}</dt>
<dd className="text-white font-medium text-right">{item.value}</dd>
</React.Fragment>
))}
</dl>
)}
</div>
<ActionBoxActions
selectedMode={actionMode}
amount={amount ?? 0}
maxAmount={maxAmount}
showCloseBalance={showCloseBalance ?? false}
handleAction={() => (showCloseBalance ? handleCloseBalance() : handleLendingAction())}
/>
{selectedToken !== null && amount !== null && preview.length > 0 && (
<dl className="grid grid-cols-2 text-muted-foreground gap-y-2 mt-4 text-sm">
{preview.map((item) => (
<React.Fragment key={item.key}>
<dt>{item.key}</dt>
<dd className="text-white font-medium text-right">{item.value}</dd>
</React.Fragment>
))}
</dl>
)}
</div>
</div>
<LSTDialog
variant={lstDialogVariant}
open={isLSTDialogOpen}
onClose={() => {
setIsLSTDialogOpen(false);
setLSTDialogVariant(null);
if (lstDialogCallback) {
lstDialogCallback();
setLSTDialogCallback(null);
}
}}
/>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,29 @@ import { ActionType } from "@mrgnlabs/marginfi-v2-ui-state";
import { useUiStore } from "~/store";

import { Button } from "~/components/ui/button";
import { IconLoader } from "~/components/ui/icons";

type ActionBoxActionsProps = {
selectedMode?: ActionType;
amount: number;
maxAmount: number;
showCloseBalance: boolean;
isLoading: boolean;
handleAction: () => void;
};

export const ActionBoxActions = ({
selectedMode,
amount,
maxAmount,
showCloseBalance,
isLoading,
handleAction,
}: ActionBoxActionsProps) => {
const [selectedToken] = useUiStore((state) => [state.selectedToken]);
const [actionMode, selectedToken] = useUiStore((state) => [state.actionMode, state.selectedToken]);

const isActionDisabled = React.useMemo(() => {
const isValidInput = amount > 0;
return (maxAmount === 0 || !isValidInput) && !showCloseBalance;
}, [amount, showCloseBalance, maxAmount]);
return ((maxAmount === 0 || !isValidInput) && !showCloseBalance) || isLoading;
}, [amount, showCloseBalance, maxAmount, isLoading]);

const actionText = React.useMemo(() => {
if (!selectedToken) {
Expand All @@ -36,9 +37,10 @@ export const ActionBoxActions = ({
if (showCloseBalance) {
return "Close account";
}
console.log({ actionMode });

if (maxAmount === 0) {
switch (selectedMode) {
switch (actionMode) {
case ActionType.Deposit:
return `Insufficient ${selectedToken.meta.tokenSymbol} in wallet`;
case ActionType.Withdraw:
Expand All @@ -56,12 +58,12 @@ export const ActionBoxActions = ({
return "Add an amount";
}

return selectedMode;
}, [selectedMode, amount, selectedToken, maxAmount, showCloseBalance]);
return actionMode;
}, [actionMode, amount, selectedToken, maxAmount, showCloseBalance]);

return (
<Button disabled={isActionDisabled} className="w-full py-6" onClick={handleAction}>
{actionText}
{isLoading ? <IconLoader /> : actionText}
</Button>
);
};
Loading

0 comments on commit c3109e7

Please sign in to comment.