From dfb117b1981a6c39a8ba98e6e964d3f74d81113d Mon Sep 17 00:00:00 2001 From: Teolhyn Date: Thu, 18 Dec 2025 14:09:28 +0200 Subject: [PATCH 1/2] feat: add better lend and borrow layout for mobile --- src/pages/_borrow/BorrowMobileCard.tsx | 85 ++++++++++++++++++++++++++ src/pages/_borrow/BorrowPage.tsx | 43 ++++++++++--- src/pages/_lend/LendMobileCard.tsx | 69 +++++++++++++++++++++ src/pages/_lend/LendPage.tsx | 43 ++++++++++--- 4 files changed, 220 insertions(+), 20 deletions(-) create mode 100644 src/pages/_borrow/BorrowMobileCard.tsx create mode 100644 src/pages/_lend/LendMobileCard.tsx diff --git a/src/pages/_borrow/BorrowMobileCard.tsx b/src/pages/_borrow/BorrowMobileCard.tsx new file mode 100644 index 00000000..215eaea0 --- /dev/null +++ b/src/pages/_borrow/BorrowMobileCard.tsx @@ -0,0 +1,85 @@ +import { useMemo } from 'react'; + +import { Button } from '@components/Button'; +import { Loading } from '@components/Loading'; +import { usePools } from '@contexts/pool-context'; +import { useWallet } from '@contexts/wallet-context'; +import { isBalanceZero } from '@lib/converters'; +import { formatAPR, formatAmount, toDollarsFormatted } from '@lib/formatting'; +import { isNil } from 'ramda'; +import type { CurrencyBinding } from 'src/currency-bindings'; + +export interface BorrowMobileCardProps { + currency: CurrencyBinding; + onBorrowClicked: VoidFunction; +} + +export const BorrowMobileCard = ({ currency, onBorrowClicked }: BorrowMobileCardProps) => { + const { icon, name, ticker, issuerName } = currency; + + const { wallet, walletBalances } = useWallet(); + const { prices, pools } = usePools(); + const pool = pools?.[ticker]; + const price = prices?.[ticker]; + + // Does the user have some other token in their wallet to use as a collateral? + const isCollateral = !walletBalances + ? false + : Object.entries(walletBalances) + .filter(([t, _b]) => t !== ticker) + .some(([_t, b]) => b.trustLine && !isBalanceZero(b.balanceLine.balance)); + + const borrowDisabled = !wallet || !isCollateral || !pool || pool.availableBalanceTokens === 0n; + + const tooltip = useMemo(() => { + if (!pool) return 'The pool is loading'; + if (pool.availableBalanceTokens === 0n) return 'the pool has no assets to borrow'; + if (!wallet) return 'Connect a wallet first'; + if (!isCollateral) return 'Another token needed for the collateral'; + return 'Something odd happened.'; + }, [pool, wallet, isCollateral]); + + return ( +
+
+ +
+

{name}

+

+ {ticker} • {issuerName} +

+
+
+ +
+
+

Available

+

+ {pool ? formatAmount(pool.availableBalanceTokens) : } +

+

+ {!isNil(price) && !isNil(pool) && toDollarsFormatted(price, pool.availableBalanceTokens)} +

+
+
+

Borrow APY

+

{pool ? formatAPR(pool.annualInterestRate) : }

+
+
+ +
+ {borrowDisabled ? ( +
+ +
+ ) : ( + + )} +
+
+ ); +}; diff --git a/src/pages/_borrow/BorrowPage.tsx b/src/pages/_borrow/BorrowPage.tsx index 964a4f2f..a0c1ea4c 100644 --- a/src/pages/_borrow/BorrowPage.tsx +++ b/src/pages/_borrow/BorrowPage.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Card } from '@components/Card'; import { StellarExpertLink } from '@components/Link'; @@ -8,6 +8,7 @@ import { usePools } from '@contexts/pool-context'; import { useWallet } from '@contexts/wallet-context'; import { contractId } from '@contracts/loan_manager'; import { CURRENCY_BINDINGS_ARR, type CurrencyBinding } from 'src/currency-bindings'; +import { BorrowMobileCard } from './BorrowMobileCard'; import { BorrowModal } from './BorrowModal/BorrowModal'; import { BorrowableAsset } from './BorrowableAsset'; @@ -20,6 +21,7 @@ const BorrowPage = () => { const { refetchBalances } = useWallet(); const { refetchPools } = usePools(); const [selectedCurrency, setSelectedCurrency] = useState(null); + const [isMobile, setIsMobile] = useState(true); const modalId = 'borrow-modal'; @@ -37,6 +39,15 @@ const BorrowPage = () => { refetchPools(); }; + useEffect(() => { + const media = window.matchMedia('(max-width: 768px)'); + setIsMobile(media.matches); + + const handler = () => setIsMobile(media.matches); + media.addEventListener('change', handler); + return () => media.removeEventListener('change', handler); + }, []); + return ( <>
@@ -44,15 +55,27 @@ const BorrowPage = () => {

Borrow Assets

- - {CURRENCY_BINDINGS_ARR.map((currency) => ( - openBorrowModal(currency)} - /> - ))} -
+ {isMobile ? ( + <> + {CURRENCY_BINDINGS_ARR.map((currency) => ( + openBorrowModal(currency)} + /> + ))} + + ) : ( + + {CURRENCY_BINDINGS_ARR.map((currency) => ( + openBorrowModal(currency)} + /> + ))} +
+ )}
diff --git a/src/pages/_lend/LendMobileCard.tsx b/src/pages/_lend/LendMobileCard.tsx new file mode 100644 index 00000000..73e7b7f4 --- /dev/null +++ b/src/pages/_lend/LendMobileCard.tsx @@ -0,0 +1,69 @@ +import { Button } from '@components/Button'; +import { Loading } from '@components/Loading'; +import { usePools } from '@contexts/pool-context'; +import { type Balance, useWallet } from '@contexts/wallet-context'; +import { isBalanceZero } from '@lib/converters'; +import { formatAPY, formatAmount, toDollarsFormatted } from '@lib/formatting'; +import { isNil } from 'ramda'; +import type { CurrencyBinding } from 'src/currency-bindings'; + +export interface LendMobileCardProps { + currency: CurrencyBinding; + onDepositClicked: VoidFunction; +} + +export const LendMobileCard = ({ currency, onDepositClicked }: LendMobileCardProps) => { + const { icon, name, ticker, issuerName } = currency; + + const { wallet, walletBalances } = useWallet(); + const { prices, pools } = usePools(); + const pool = pools?.[ticker]; + const price = prices?.[ticker]; + const balance: Balance | undefined = walletBalances?.[ticker]; + + const isPoor = !balance?.trustLine || isBalanceZero(balance.balanceLine.balance); + + return ( +
+
+ +
+

{name}

+

+ {ticker} • {issuerName} +

+
+
+ +
+
+

Balance

+

+ {pool ? formatAmount(pool.totalBalanceTokens) : } +

+

+ {!isNil(price) && !isNil(pool) && toDollarsFormatted(price, pool.totalBalanceTokens)} +

+
+
+

Supply APY

+

{pool ? formatAPY(pool.annualInterestRate) : }

+
+
+ +
+ {isPoor ? ( +
+ +
+ ) : ( + + )} +
+
+ ); +}; diff --git a/src/pages/_lend/LendPage.tsx b/src/pages/_lend/LendPage.tsx index ba787418..947831c5 100644 --- a/src/pages/_lend/LendPage.tsx +++ b/src/pages/_lend/LendPage.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Card } from '@components/Card'; import { StellarExpertLink } from '@components/Link'; @@ -8,6 +8,7 @@ import { usePools } from '@contexts/pool-context'; import { contractId } from '@contracts/loan_manager'; import { CURRENCY_BINDINGS_ARR, type CurrencyBinding } from 'src/currency-bindings'; import { DepositModal } from './DepositModal'; +import { LendMobileCard } from './LendMobileCard'; import { LendableAsset } from './LendableAsset'; const links = [ @@ -18,6 +19,7 @@ const links = [ const LendPage = () => { const { refetchPools } = usePools(); const [selectedCurrency, setSelectedCurrency] = useState(null); + const [isMobile, setIsMobile] = useState(true); const modalId = 'deposit-modal'; @@ -34,6 +36,15 @@ const LendPage = () => { refetchPools(); }; + useEffect(() => { + const media = window.matchMedia('(max-width: 768px)'); + setIsMobile(media.matches); + + const handler = () => setIsMobile(media.matches); + media.addEventListener('change', handler); + return () => media.removeEventListener('change', handler); + }, []); + return ( <>
@@ -41,15 +52,27 @@ const LendPage = () => {

Lend Assets

- - {CURRENCY_BINDINGS_ARR.map((currency) => ( - openDepositModal(currency)} - /> - ))} -
+ {isMobile ? ( + <> + {CURRENCY_BINDINGS_ARR.map((currency) => ( + openDepositModal(currency)} + /> + ))} + + ) : ( + + {CURRENCY_BINDINGS_ARR.map((currency) => ( + openDepositModal(currency)} + /> + ))} +
+ )}
From b5defcb3779202d73656584ffc163d786812ca26 Mon Sep 17 00:00:00 2001 From: Teolhyn Date: Thu, 18 Dec 2025 15:06:11 +0200 Subject: [PATCH 2/2] fix: change from js conditional rendering to pure css conditionals --- src/pages/_borrow/BorrowPage.tsx | 35 +++++++++++--------------------- src/pages/_lend/LendPage.tsx | 35 +++++++++++--------------------- 2 files changed, 24 insertions(+), 46 deletions(-) diff --git a/src/pages/_borrow/BorrowPage.tsx b/src/pages/_borrow/BorrowPage.tsx index a0c1ea4c..8d01c31f 100644 --- a/src/pages/_borrow/BorrowPage.tsx +++ b/src/pages/_borrow/BorrowPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { Card } from '@components/Card'; import { StellarExpertLink } from '@components/Link'; @@ -21,7 +21,6 @@ const BorrowPage = () => { const { refetchBalances } = useWallet(); const { refetchPools } = usePools(); const [selectedCurrency, setSelectedCurrency] = useState(null); - const [isMobile, setIsMobile] = useState(true); const modalId = 'borrow-modal'; @@ -39,15 +38,6 @@ const BorrowPage = () => { refetchPools(); }; - useEffect(() => { - const media = window.matchMedia('(max-width: 768px)'); - setIsMobile(media.matches); - - const handler = () => setIsMobile(media.matches); - media.addEventListener('change', handler); - return () => media.removeEventListener('change', handler); - }, []); - return ( <>
@@ -55,17 +45,16 @@ const BorrowPage = () => {

Borrow Assets

- {isMobile ? ( - <> - {CURRENCY_BINDINGS_ARR.map((currency) => ( - openBorrowModal(currency)} - /> - ))} - - ) : ( +
+ {CURRENCY_BINDINGS_ARR.map((currency) => ( + openBorrowModal(currency)} + /> + ))} +
+
{CURRENCY_BINDINGS_ARR.map((currency) => ( { /> ))}
- )} +
diff --git a/src/pages/_lend/LendPage.tsx b/src/pages/_lend/LendPage.tsx index 947831c5..ee5f83d3 100644 --- a/src/pages/_lend/LendPage.tsx +++ b/src/pages/_lend/LendPage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { Card } from '@components/Card'; import { StellarExpertLink } from '@components/Link'; @@ -19,7 +19,6 @@ const links = [ const LendPage = () => { const { refetchPools } = usePools(); const [selectedCurrency, setSelectedCurrency] = useState(null); - const [isMobile, setIsMobile] = useState(true); const modalId = 'deposit-modal'; @@ -36,15 +35,6 @@ const LendPage = () => { refetchPools(); }; - useEffect(() => { - const media = window.matchMedia('(max-width: 768px)'); - setIsMobile(media.matches); - - const handler = () => setIsMobile(media.matches); - media.addEventListener('change', handler); - return () => media.removeEventListener('change', handler); - }, []); - return ( <>
@@ -52,17 +42,16 @@ const LendPage = () => {

Lend Assets

- {isMobile ? ( - <> - {CURRENCY_BINDINGS_ARR.map((currency) => ( - openDepositModal(currency)} - /> - ))} - - ) : ( +
+ {CURRENCY_BINDINGS_ARR.map((currency) => ( + openDepositModal(currency)} + /> + ))} +
+
{CURRENCY_BINDINGS_ARR.map((currency) => ( { /> ))}
- )} +