Skip to content

Commit 2540fdf

Browse files
feat: borrow usdc modal (#291)
* feat(packages): borrow activity card
1 parent 87f2a14 commit 2540fdf

File tree

8 files changed

+537
-11
lines changed

8 files changed

+537
-11
lines changed

package-lock.json

Lines changed: 66 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

routes/vault/src/assets/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Asset exports for vault application
2+
3+
// USDC icon as data URI - Simple circle design with USDC branding
4+
export const usdcIcon = 'data:image/svg+xml,%3Csvg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"%3E%3Ccircle cx="20" cy="20" r="20" fill="%232775CA"/%3E%3Cpath d="M24.0001 17.3999C24.0001 15.6399 22.8001 14.7999 20.4001 14.5999V12.3999H18.8001V14.5599C18.3601 14.5599 17.9201 14.5599 17.4801 14.5999V12.3999H15.8801V14.5999C15.5601 14.5999 15.2401 14.6399 14.9201 14.6399H13.2001V16.3599H14.4401C14.8801 16.3599 15.0801 16.5999 15.0801 16.9599V23.0399C15.0801 23.3999 14.8801 23.6399 14.4401 23.6399H13.2001V25.7599L14.8401 25.7999C15.1601 25.7999 15.4801 25.7999 15.8001 25.8399V28.0399H17.4001V25.8799C17.8401 25.8799 18.2801 25.9199 18.7201 25.9199V28.0799H20.3201V25.8799C23.2001 25.7199 24.8001 24.5199 24.8001 22.3599C24.8001 20.7999 24.0001 19.8799 22.5601 19.4799C23.5201 19.0799 24.0001 18.3599 24.0001 17.3999ZM18.7201 21.1999V23.5199C17.7201 23.4399 17.2001 23.3999 16.6801 23.3599V21.1999C17.2001 21.1599 17.7201 21.1199 18.7201 21.1999ZM20.3201 16.7199C21.2401 16.7999 21.7601 16.8399 22.3201 16.9199V19.0399C21.7601 18.9599 21.2401 18.9199 20.3201 18.8399V16.7199ZM20.3201 23.5599V21.1599C21.3201 21.2399 21.8401 21.2799 22.4001 21.3599C22.6001 22.3599 21.8401 23.1999 20.3201 23.5599Z" fill="white"/%3E%3C/svg%3E';
5+

routes/vault/src/assets/usdc.svg

Lines changed: 9 additions & 0 deletions
Loading
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import {
2+
Button,
3+
Dialog,
4+
MobileDialog,
5+
DialogBody,
6+
DialogFooter,
7+
DialogHeader,
8+
Text,
9+
useIsMobile,
10+
AmountItem,
11+
SubSection,
12+
} from "@babylonlabs-io/core-ui";
13+
import { useMemo, type ReactNode } from "react";
14+
import { twMerge } from "tailwind-merge";
15+
import { useBorrowService } from "../../hooks/useBorrowService";
16+
import { usdcIcon } from "../../assets";
17+
18+
type DialogComponentProps = Parameters<typeof Dialog>[0];
19+
20+
interface ResponsiveDialogProps extends DialogComponentProps {
21+
children?: ReactNode;
22+
}
23+
24+
function ResponsiveDialog({ className, ...restProps }: ResponsiveDialogProps) {
25+
const isMobileView = useIsMobile(640);
26+
const DialogComponent = isMobileView ? MobileDialog : Dialog;
27+
28+
return (
29+
<DialogComponent {...restProps} className={twMerge("w-[41.25rem] max-w-full", className)} />
30+
);
31+
}
32+
33+
interface BorrowModalProps {
34+
open: boolean;
35+
onClose: () => void;
36+
collateral: {
37+
amount: string;
38+
symbol: string;
39+
icon?: string | ReactNode;
40+
};
41+
}
42+
43+
export function BorrowModal({ open, onClose, collateral }: BorrowModalProps) {
44+
const collateralBTC = useMemo(
45+
() => parseFloat(collateral.amount || "0"),
46+
[collateral.amount]
47+
);
48+
49+
const collateralIconUrl = useMemo(() => {
50+
if (typeof collateral.icon === "string") {
51+
return collateral.icon;
52+
}
53+
return "";
54+
}, [collateral.icon]);
55+
56+
const {
57+
borrowAmount,
58+
borrowAmountNum,
59+
processing,
60+
inputState,
61+
maxBorrow,
62+
collateralValueUSD,
63+
currentLTV,
64+
validation,
65+
hintText,
66+
btcPriceUSD,
67+
usdcPriceUSD,
68+
maxLTV,
69+
liquidationLTV,
70+
handleInputChange,
71+
handleBorrow,
72+
setTouched,
73+
formatUSD,
74+
formatPercentage,
75+
} = useBorrowService(collateralBTC);
76+
77+
// Handle key down to prevent arrow keys
78+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
79+
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
80+
e.preventDefault();
81+
}
82+
};
83+
84+
// Handle input change for collateral (read-only)
85+
const handleCollateralChange = () => {
86+
// No-op, collateral is read-only
87+
};
88+
89+
// Handle borrow button click
90+
const handleBorrowClick = async () => {
91+
setTouched(true);
92+
if (validation.isValid && borrowAmountNum > 0) {
93+
await handleBorrow(borrowAmountNum, collateralBTC);
94+
onClose();
95+
}
96+
};
97+
98+
return (
99+
<ResponsiveDialog open={open} onClose={onClose}>
100+
<DialogHeader title="Collateral" onClose={onClose} className="text-accent-primary" />
101+
<DialogBody className="no-scrollbar mb-8 mt-4 flex max-h-[calc(100vh-12rem)] flex-col gap-6 overflow-y-auto text-accent-primary px-4 sm:px-6">
102+
{/* Subtitle */}
103+
<Text variant="body2" className="text-accent-secondary -mt-2 text-sm sm:text-base">
104+
Your locked BTC collateral
105+
</Text>
106+
107+
{/* Collateral Display */}
108+
<SubSection className="flex w-full flex-col content-center justify-between gap-4">
109+
<AmountItem
110+
amount={collateral.amount}
111+
currencyIcon={collateralIconUrl}
112+
currencyName="BTC"
113+
placeholder=""
114+
displayBalance={true}
115+
balanceDetails={{
116+
balance: parseFloat(collateral.amount).toFixed(2),
117+
symbol: "BTC",
118+
price: btcPriceUSD,
119+
displayUSD: true,
120+
}}
121+
min="0"
122+
step="any"
123+
autoFocus={false}
124+
onChange={handleCollateralChange}
125+
onKeyDown={handleKeyDown}
126+
amountUsd={formatUSD(collateralValueUSD)}
127+
subtitle={`Balance: ${parseFloat(collateral.amount).toFixed(2)} BTC`}
128+
disabled={true}
129+
/>
130+
</SubSection>
131+
132+
{/* Borrow Section */}
133+
<div className="flex flex-col gap-2">
134+
<h3 className="text-base sm:text-lg font-semibold text-accent-primary">
135+
Borrow
136+
</h3>
137+
<Text variant="body2" className="text-accent-secondary text-sm sm:text-base">
138+
Enter the amount you want to borrow
139+
</Text>
140+
</div>
141+
142+
{/* Borrow Amount Input */}
143+
<SubSection className="flex w-full flex-col content-center justify-between gap-4">
144+
<AmountItem
145+
amount={borrowAmount}
146+
currencyIcon={usdcIcon}
147+
currencyName="USDC"
148+
placeholder="0"
149+
displayBalance={true}
150+
balanceDetails={{
151+
balance: maxBorrow.toFixed(0),
152+
symbol: "USDC",
153+
price: usdcPriceUSD,
154+
displayUSD: true,
155+
}}
156+
min="0"
157+
step="any"
158+
autoFocus={true}
159+
onChange={handleInputChange}
160+
onKeyDown={handleKeyDown}
161+
amountUsd={formatUSD(borrowAmountNum)}
162+
subtitle={`Max: ${maxBorrow.toFixed(0)} USDC`}
163+
/>
164+
{hintText && (
165+
<Text
166+
variant="body2"
167+
className={twMerge(
168+
"text-xs sm:text-sm -mt-2",
169+
inputState === "error" && "text-error-main",
170+
inputState === "warning" && "text-warning-main"
171+
)}
172+
>
173+
{hintText}
174+
</Text>
175+
)}
176+
</SubSection>
177+
178+
{/* Metrics Card */}
179+
<div className="flex flex-col gap-3 p-3 sm:p-4 bg-secondary-highlight rounded">
180+
<div className="flex items-center justify-between gap-2">
181+
<Text variant="body2" className="text-accent-secondary text-xs sm:text-sm shrink-0">
182+
Collateral
183+
</Text>
184+
<Text variant="body1" className="font-medium text-xs sm:text-sm text-right truncate">
185+
{collateral.amount} BTC ({formatUSD(collateralValueUSD)})
186+
</Text>
187+
</div>
188+
<div className="flex items-center justify-between gap-2">
189+
<Text variant="body2" className="text-accent-secondary text-xs sm:text-sm shrink-0">
190+
Loan
191+
</Text>
192+
<Text variant="body1" className="font-medium text-xs sm:text-sm text-right truncate">
193+
{borrowAmount || "0"} USDC ({formatUSD(borrowAmountNum)})
194+
</Text>
195+
</div>
196+
<div className="flex items-center justify-between gap-2">
197+
<Text variant="body2" className="text-accent-secondary text-xs sm:text-sm shrink-0">
198+
LTV
199+
</Text>
200+
<Text
201+
variant="body1"
202+
className={twMerge(
203+
"font-medium text-xs sm:text-sm",
204+
currentLTV > 50 && "text-warning-main",
205+
currentLTV > maxLTV * 100 && "text-error-main"
206+
)}
207+
>
208+
{formatPercentage(currentLTV)}
209+
</Text>
210+
</div>
211+
<div className="flex items-center justify-between gap-2">
212+
<Text variant="body2" className="text-accent-secondary text-xs sm:text-sm shrink-0">
213+
Liquidation LTV
214+
</Text>
215+
<Text variant="body1" className="font-medium text-xs sm:text-sm">
216+
{formatPercentage(liquidationLTV * 100)}
217+
</Text>
218+
</div>
219+
</div>
220+
</DialogBody>
221+
<DialogFooter className="flex flex-col gap-4 pb-8 pt-0">
222+
<Button
223+
variant="contained"
224+
color="primary"
225+
onClick={handleBorrowClick}
226+
className="w-full"
227+
disabled={!validation.isValid || borrowAmountNum === 0 || processing}
228+
>
229+
{processing ? "Processing..." : "Borrow"}
230+
</Button>
231+
</DialogFooter>
232+
</ResponsiveDialog>
233+
);
234+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Modal components for vault application
2+
export { BorrowModal } from "./BorrowModal";

routes/vault/src/components/ui/Borrow.tsx

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,30 @@ import {
88
} from "@babylonlabs-io/core-ui";
99
import { useState } from "react";
1010
import { mockVaultActivities, type VaultActivity } from "../../mockData/vaultActivities";
11+
import { BorrowModal } from "../modals";
1112

1213
export function Borrow() {
1314
const [activities] = useState<VaultActivity[]>(mockVaultActivities);
15+
const [modalOpen, setModalOpen] = useState(false);
16+
const [selectedActivity, setSelectedActivity] = useState<VaultActivity | null>(null);
1417

1518
const handleNewBorrow = () => {
16-
console.log("New borrow clicked");
17-
// TODO: Open modal to create new borrow
19+
if (activities.length > 0) {
20+
// TODO: getSelectedActivity method should be implemented
21+
setSelectedActivity(activities[0]);
22+
setModalOpen(true);
23+
}
24+
};
25+
26+
const handleActivityBorrow = (activity: VaultActivity) => {
27+
setSelectedActivity(activity);
28+
setModalOpen(true);
29+
};
30+
31+
// Handle modal close
32+
const handleModalClose = () => {
33+
setModalOpen(false);
34+
setSelectedActivity(null);
1835
};
1936

2037
// Transform vault activities to ActivityCard data format
@@ -53,19 +70,30 @@ export function Borrow() {
5370
details: [statusDetail, providersDetail],
5471
primaryAction: {
5572
label: activity.action.label,
56-
onClick: activity.action.onClick,
73+
onClick: () => handleActivityBorrow(activity),
5774
},
5875
};
5976
});
6077

6178
return (
62-
<div className="container mx-auto flex max-w-[760px] flex-1 flex-col px-4 py-8">
63-
<BorrowCard onNewBorrow={handleNewBorrow}>
64-
{activityCardData.map((data, index) => (
65-
<ActivityCard key={activities[index].id} data={data} />
66-
))}
67-
</BorrowCard>
68-
</div>
79+
<>
80+
<div className="container mx-auto flex max-w-[760px] flex-1 flex-col px-4 py-8">
81+
<BorrowCard onNewBorrow={handleNewBorrow}>
82+
{activityCardData.map((data, index) => (
83+
<ActivityCard key={activities[index].id} data={data} />
84+
))}
85+
</BorrowCard>
86+
</div>
87+
88+
{/* Borrow Modal */}
89+
{selectedActivity && (
90+
<BorrowModal
91+
open={modalOpen}
92+
onClose={handleModalClose}
93+
collateral={selectedActivity.collateral}
94+
/>
95+
)}
96+
</>
6997
);
7098
}
7199

0 commit comments

Comments
 (0)