ui: small visual adaptions after placeholder PR (#222)

* chore: harmonize sprite definitions

* fix: prevent render errors on invalid token in CurrentWalletMagic screen

* ui: add border-radius to input placeholder

* fix: add relationship 'noopener' to external links on Send screen

Co-authored-by: Đorđe Spasić <34274884+Djo1e@users.noreply.github.com>
This commit is contained in:
Thebora Kompanioni 2022-04-20 10:45:12 +02:00 committed by GitHub
parent bb007f841f
commit 34bfd0b3cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 131 additions and 111 deletions

View file

@ -44,16 +44,16 @@
<path d="M8 9.5a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/>
<path d="M9.5 2c-.9 0-1.75.216-2.501.6A5 5 0 0 1 13 7.5a6.5 6.5 0 1 1-13 0 .5.5 0 0 1 1 0 5.5 5.5 0 0 0 8.001 4.9A5 5 0 0 1 3 7.5a6.5 6.5 0 0 1 13 0 .5.5 0 0 1-1 0A5.5 5.5 0 0 0 9.5 2zM8 3.5a4 4 0 1 0 0 8 4 4 0 0 0 0-8z"/>
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" id="welcome" aria-hidden="true" class="svg-inline--fa fa-door-open fa-w-20" data-icon="door-open" data-prefix="fas" focusable="false" role="img" viewBox="0 0 640 512">
<symbol id="welcome" viewBox="0 0 640 512">
<path fill="currentColor" d="M624 448h-80V113.45C544 86.19 522.47 64 496 64H384v64h96v384h144c8.84 0 16-7.16 16-16v-32c0-8.84-7.16-16-16-16zM312.24 1.01l-192 49.74C105.99 54.44 96 67.7 96 82.92V448H16c-8.84 0-16 7.16-16 16v32c0 8.84 7.16 16 16 16h336V33.18c0-21.58-19.56-37.41-39.76-32.17zM264 288c-13.25 0-24-14.33-24-32s10.75-32 24-32 24 14.33 24 32-10.75 32-24 32z"/>
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" id="collab" aria-hidden="true" class="svg-inline--fa fa-compress-arrows-alt fa-w-16" data-icon="compress-arrows-alt" data-prefix="fas" focusable="false" role="img" viewBox="0 0 512 512">
<symbol id="collab" viewBox="0 0 512 512">
<path fill="currentColor" d="M200 288H88c-21.4 0-32.1 25.8-17 41l32.9 31-99.2 99.3c-6.2 6.2-6.2 16.4 0 22.6l25.4 25.4c6.2 6.2 16.4 6.2 22.6 0L152 408l31.1 33c15.1 15.1 40.9 4.4 40.9-17V312c0-13.3-10.7-24-24-24zm112-64h112c21.4 0 32.1-25.9 17-41l-33-31 99.3-99.3c6.2-6.2 6.2-16.4 0-22.6L481.9 4.7c-6.2-6.2-16.4-6.2-22.6 0L360 104l-31.1-33C313.8 55.9 288 66.6 288 88v112c0 13.3 10.7 24 24 24zm96 136l33-31.1c15.1-15.1 4.4-40.9-17-40.9H312c-13.3 0-24 10.7-24 24v112c0 21.4 25.9 32.1 41 17l31-32.9 99.3 99.3c6.2 6.2 16.4 6.2 22.6 0l25.4-25.4c6.2-6.2 6.2-16.4 0-22.6L408 360zM183 71.1L152 104 52.7 4.7c-6.2-6.2-16.4-6.2-22.6 0L4.7 30.1c-6.2 6.2-6.2 16.4 0 22.6L104 152l-33 31.1C55.9 198.2 66.6 224 88 224h112c13.3 0 24-10.7 24-24V88c0-21.3-25.9-32-41-16.9z"/>
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" id="key" fill="currentColor" data-v-4fa90e7f="" viewBox="0 0 24 24">
<symbol id="key" fill="currentColor" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M18.865 7.374c.74 2.968-1.163 5.998-4.25 6.767a6.01 6.01 0 01-1.155.172l-3.435 5.471a2 2 0 01-1.21.877l-1.974.492a1 1 0 01-1.212-.728l-.445-1.786a2 2 0 01.246-1.547l3.157-5.028a5.338 5.338 0 01-.9-1.903c-.74-2.968 1.162-5.998 4.249-6.768 3.087-.77 6.189 1.013 6.929 3.98zm-4.627 1.324c.685-.171 1.108-.844.944-1.504-.165-.66-.854-1.055-1.54-.884-.686.17-1.109.844-.944 1.503.164.66.854 1.056 1.54.885z" clip-rule="evenodd"/>
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" id="handshake" aria-hidden="true" class="svg-inline--fa fa-handshake-slash fa-w-20" data-icon="handshake-slash" data-prefix="fas" focusable="false" role="img" viewBox="0 0 640 512">
<symbol id="handshake" viewBox="0 0 640 512">
<path fill="currentColor" d="M0,128.21V384H64a32,32,0,0,0,32-32V184L23.83,128.21ZM48,320.1a16,16,0,1,1-16,16A16,16,0,0,1,48,320.1Zm80,31.81h18.3l90.5,81.89a64,64,0,0,0,90-9.3l.2-.2,17.91,15.5a37.16,37.16,0,0,0,52.29-5.39l8.8-10.82L128,208.72Zm416-223.7V352.1a32,32,0,0,0,32,32h64V128.21ZM592,352.1a16,16,0,1,1,16-16A16,16,0,0,1,592,352.1ZM303.33,202.67l59.58-54.57a16,16,0,0,1,21.59,23.61L358.41,195.6,504,313.8a73.08,73.08,0,0,1,7.91,7.7V128L457.3,73.41A31.76,31.76,0,0,0,434.7,64H348.8a31.93,31.93,0,0,0-21.6,8.41l-88.07,80.64-25.64-19.81L289.09,64H205.3a32,32,0,0,0-22.6,9.41L162.36,93.72,45.47,3.38A16,16,0,0,0,23,6.19L3.38,31.46A16,16,0,0,0,6.19,53.91L594.53,508.63A16,16,0,0,0,617,505.82l19.65-25.27a16,16,0,0,0-2.82-22.45Z"/>
</symbol>
<symbol id="shield-outline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -62,7 +62,7 @@
<symbol id="shield-filled" fill="currentColor" viewBox="0 0 24 24">
<path d="M5 7c0-.276.225-.499.498-.535 2.149-.28 5.282-2.186 6.224-2.785a.516.516 0 01.556 0c.942.599 4.075 2.504 6.224 2.785.273.036.498.259.498.535v4.75c0 6.5-7 8.75-7 8.75s-7-2.25-7-8.75V7z"/>
</symbol>
<symbol id="shield-filled-loading" fill="#e9ecef" viewBox="0 0 24 24">
<symbol id="shield-filled-loading" fill="currentColor" viewBox="0 0 24 24">
<path d="M5 7c0-.276.225-.499.498-.535 2.149-.28 5.282-2.186 6.224-2.785a.516.516 0 01.556 0c.942.599 4.075 2.504 6.224 2.785.273.036.498.259.498.535v4.75c0 6.5-7 8.75-7 8.75s-7-2.25-7-8.75V7z"/>
</symbol>
<symbol id="wand" fill="currentColor" viewBox="0 0 24 24">
@ -81,7 +81,7 @@
<symbol id="caret-down" fill="currentColor" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M4.47 9.4a.75.75 0 011.06 0l6.364 6.364a.25.25 0 00.354 0L18.612 9.4a.75.75 0 011.06 1.06l-6.364 6.364a1.75 1.75 0 01-2.475 0L4.47 10.46a.75.75 0 010-1.06z" clip-rule="evenodd"/>
</symbol>
<symbol xmlns="http://www.w3.org/2000/svg" id="mnemonic" fill="currentColor" viewBox="0 0 24 24">
<symbol id="mnemonic" fill="currentColor" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M5 6.5A1.5 1.5 0 016.5 5h11A1.5 1.5 0 0119 6.5v11a1.5 1.5 0 01-1.5 1.5h-11A1.5 1.5 0 015 17.5v-11zm1.5 0H9V8H6.5V6.5zM9 9.667H6.5v1.5H9v-1.5zm-2.5 3.166H9v1.5H6.5v-1.5zM9 16H6.5v1.5H9V16zm1.75-9.5h2.5V8h-2.5V6.5zm2.5 3.167h-2.5v1.5h2.5v-1.5zm-2.5 3.166h2.5v1.5h-2.5v-1.5zM13.25 16h-2.5v1.5h2.5V16zM15 6.5h2.5V8H15V6.5zm2.5 3.167H15v1.5h2.5v-1.5zM15 12.833h2.5v1.5H15v-1.5zM17.5 16H15v1.5h2.5V16z" clip-rule="evenodd"/>
</symbol>
<symbol id="globe" fill="currentColor" viewBox="0 0 24 24">

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Before After
Before After

View file

@ -45,15 +45,7 @@ const formatSats = (value) => {
return formatter.format(value)
}
const BalanceComponent = ({ symbol, value, symbolIsPrefix, loading }) => {
if (loading) {
return (
<rb.Placeholder as="p" animation="wave" className="mb-0">
<rb.Placeholder className={styles['balance-component-placeholder']} />
</rb.Placeholder>
)
}
const BalanceComponent = ({ symbol, value, symbolIsPrefix }) => {
return (
<span className="d-inline-flex align-items-center">
{symbolIsPrefix && symbol}
@ -75,15 +67,27 @@ const BalanceComponent = ({ symbol, value, symbolIsPrefix, loading }) => {
* Possible options are `BTC` and `SATS` from `src/utils.js`
* @param {showBalance}: A flag indicating whether to render or hide the balance.
* Hidden balances are masked with `*****`.
* @param {loading}: A loading flag that renders a placeholder while true.
*/
export default function Balance({ valueString, convertToUnit, showBalance = false, loading }) {
export default function Balance({ valueString, convertToUnit, showBalance = false, loading = false }) {
const [displayMode, setDisplayMode] = useState(DISPLAY_MODE_HIDDEN)
useEffect(() => {
setDisplayMode(getDisplayMode(convertToUnit, showBalance))
}, [convertToUnit, showBalance])
if (displayMode === DISPLAY_MODE_HIDDEN && !loading) {
if (loading) {
return (
<rb.Placeholder as="div" animation="wave">
<rb.Placeholder
data-testid="balance-component-placeholder"
className={styles['balance-component-placeholder']}
/>
</rb.Placeholder>
)
}
if (displayMode === DISPLAY_MODE_HIDDEN) {
return (
<BalanceComponent
symbol={
@ -98,8 +102,8 @@ export default function Balance({ valueString, convertToUnit, showBalance = fals
}
if (typeof valueString !== 'string') {
if (!loading) console.warn('<Balance /> component expects string input')
return <BalanceComponent symbol="" value={valueString} symbolIsPrefix={false} loading={loading} />
console.warn('<Balance /> component expects string input')
return <BalanceComponent symbol="" value={valueString} symbolIsPrefix={false} />
}
// Treat integers as sats.

View file

@ -5,6 +5,11 @@ import { BTC, SATS } from '../utils'
import Balance from './Balance'
describe('<Balance />', () => {
it('should render placeholder while loading', () => {
render(<Balance loading={true} />)
expect(screen.getByTestId('balance-component-placeholder')).toBeInTheDocument()
})
it('should render BTC using satscomma formatting', () => {
render(<Balance valueString={'123.456'} convertToUnit={BTC} showBalance={true} />)
expect(screen.getByText(`123.45 600 000`)).toBeInTheDocument()

View file

@ -14,19 +14,19 @@ const WalletHeader = ({ name, balance, unit, showBalance, loading }) => {
return (
<div className="d-flex flex-column align-items-center">
{loading && (
<rb.Placeholder as="p" animation="wave" className="mb-0">
<rb.Placeholder className={styles['wallet-header-title']} />
<rb.Placeholder as="div" animation="wave">
<rb.Placeholder className={styles['wallet-header-title-placeholder']} />
</rb.Placeholder>
)}
{!loading && <h1 className="text-secondary fs-6">{walletDisplayName(name)}</h1>}
{loading && (
<rb.Placeholder as="p" animation="wave" className="mb-0">
<rb.Placeholder className={styles['wallet-header-subtitle']} />
<rb.Placeholder as="div" animation="wave">
<rb.Placeholder className={styles['wallet-header-subtitle-placeholder']} />
</rb.Placeholder>
)}
{!loading && (
<h2>
<Balance valueString={balance} convertToUnit={unit} showBalance={showBalance || false} loading={loading} />
<Balance valueString={balance} convertToUnit={unit} showBalance={showBalance || false} />
</h2>
)}
</div>
@ -34,23 +34,26 @@ const WalletHeader = ({ name, balance, unit, showBalance, loading }) => {
}
const PrivacyLevels = ({ accounts, loading }) => {
const sortedAccounts = accounts?.sort((lhs, rhs) => lhs.account - rhs.account)
const numAccounts = sortedAccounts?.length
const numPrivacyLevelsPalceholders = 5
const sortedAccounts = (accounts || []).sort((lhs, rhs) => lhs.account - rhs.account)
const numAccounts = sortedAccounts.length
return (
<div className="d-flex justify-content-center">
<div className="d-flex flex-column align-items-start" style={{ gap: '1rem' }}>
{loading && [...new Array(5)].map((a, index) => <LoadingPrivacyLevel key={index} level={5} />)}
{!loading &&
sortedAccounts.map(({ account, account_balance: balance, branches }) => (
<PrivacyLevel
key={account}
numAccounts={numAccounts}
level={parseInt(account)}
balance={balance}
loading={loading}
/>
))}
{loading
? Array(numPrivacyLevelsPalceholders)
.fill('')
.map((_, index) => <LoadingPrivacyLevel key={index} level={numPrivacyLevelsPalceholders} />)
: sortedAccounts.map(({ account, account_balance: balance }) => (
<PrivacyLevel
key={account}
numAccounts={numAccounts}
level={parseInt(account)}
balance={balance}
loading={loading}
/>
))}
</div>
</div>
)
@ -67,13 +70,13 @@ const LoadingPrivacyLevel = ({ level }) => {
<div className="d-flex align-items-center">
<div className="d-flex">{loadingShields}</div>
<div className="ps-2">
<Balance valueString={undefined} convertToUnit={undefined} showBalance={undefined} loading={true} />
<Balance loading={true} />
</div>
</div>
)
}
const PrivacyLevel = ({ numAccounts, level, balance, loading }) => {
const PrivacyLevel = ({ numAccounts, level, balance }) => {
const settings = useSettings()
const filledShields = Array(level + 1)
@ -148,21 +151,27 @@ export default function CurrentWalletMagic() {
</rb.Row>
<rb.Row className="mt-4">
<rb.Col>
{/* Always receive on first mixdepth. */}
<ExtendedLink
disabled={isLoading}
to={routes.receive}
state={{ account: 0 }}
className="btn btn-outline-dark w-100"
>
{t('current_wallet.button_deposit')}
{/* Todo: Withdrawing needs to factor in the privacy levels as well.
Depending on the mixdepth/account there will be different amounts available. */}
<ExtendedLink to={routes.send} className="btn btn-outline-dark w-100" disabled={isLoading}>
<div className="d-flex justify-content-center align-items-center">
<Sprite symbol="send" width="24" height="24" />
<div className="ps-1">{t('current_wallet.button_withdraw')}</div>
</div>
</ExtendedLink>
</rb.Col>
<rb.Col>
{/* Todo: Withdrawing needs to factor in the privacy levels as well.
Depending on the mixdepth/account there will be different amounts available. */}
<ExtendedLink disabled={isLoading} to={routes.send} className="btn btn-outline-dark w-100">
{t('current_wallet.button_withdraw')}
{/* Always receive on first mixdepth. */}
<ExtendedLink
to={routes.receive}
state={{ account: 0 }}
className="btn btn-outline-dark w-100"
disabled={isLoading}
>
<div className="d-flex justify-content-center align-items-center">
<Sprite symbol="receive" width="24" height="24" />
<div className="ps-1">{t('current_wallet.button_deposit')}</div>
</div>
</ExtendedLink>
</rb.Col>
</rb.Row>

View file

@ -1,9 +1,9 @@
.wallet-header-title {
.wallet-header-title-placeholder {
width: 5rem;
margin-bottom: 0.5rem;
}
.wallet-header-subtitle {
.wallet-header-subtitle-placeholder {
width: 12rem;
height: 1.8rem;
margin-bottom: 0.45rem;

View file

@ -26,7 +26,7 @@ const WalletPreview = ({ wallet, walletInfo, unit, showBalance }) => {
/>
</div>
) : (
<Balance value="0.00000000" unit="BTC" showBalance={false} loading={true} />
<Balance loading={true} />
)}
</div>
</div>

View file

@ -36,12 +36,15 @@ export default function Receive() {
setAlert(null)
setIsLoading(true)
Api.getAddressNew({ walletName, mixdepth: account, token, signal: abortCtrl.signal })
.then((res) => (res.ok ? res.json() : Api.Helper.throwError(res, t('receive.error_loading_address_failed'))))
.then((data) => setAddress(data.address))
.catch((err) => {
!abortCtrl.signal.aborted && setAlert({ variant: 'danger', message: err.message })
})
// show the loader a little longer to avoid flickering
.then((_) => new Promise((r) => setTimeout(r, 200)))
.finally(() => !abortCtrl.signal.aborted && setIsLoading(false))
return () => abortCtrl.abort()
@ -106,8 +109,8 @@ export default function Receive() {
<div className={styles['qr-container']}>
{!isLoading && address && <BitcoinQR address={address} sats={amount} />}
{(isLoading || !address) && (
<rb.Placeholder as="p" animation="wave" className={styles['receive-placeholder-qr-container']}>
<rb.Placeholder xs={12} className={styles['receive-placeholder-qr']} />
<rb.Placeholder as="div" animation="wave" className={styles['receive-placeholder-qr-container']}>
<rb.Placeholder className={styles['receive-placeholder-qr']} />
</rb.Placeholder>
)}
</div>
@ -115,7 +118,7 @@ export default function Receive() {
{address && <rb.Card.Text className="text-center slashed-zeroes">{address}</rb.Card.Text>}
{!address && (
<rb.Placeholder as="p" animation="wave" className={styles['receive-placeholder-container']}>
<rb.Placeholder xs={8} className={styles['receive-placeholder']} />
<rb.Placeholder xs={12} sm={10} md={8} className={styles['receive-placeholder']} />
</rb.Placeholder>
)}
<div className="d-flex justify-content-center" style={{ gap: '1rem' }}>
@ -133,7 +136,7 @@ export default function Receive() {
}
)
}}
disabled={!address}
disabled={!address || isLoading}
>
{showAddressCopiedConfirmation ? (
<>
@ -161,17 +164,14 @@ export default function Receive() {
</rb.Card>
</div>
<div className={styles['settings-container']}>
{!isLoading && (
<rb.Button
variant={`${settings.theme}`}
className="ps-0 border-0 d-flex align-items-center"
onClick={() => setShowSettings(!showSettings)}
disabled={isLoading}
>
{t('receive.button_settings')}
<Sprite symbol={`caret-${showSettings ? 'up' : 'down'}`} className="ms-1" width="20" height="20" />
</rb.Button>
)}
<rb.Button
variant={`${settings.theme}`}
className="ps-0 border-0 d-flex align-items-center"
onClick={() => setShowSettings(!showSettings)}
>
{t('receive.button_settings')}
<Sprite symbol={`caret-${showSettings ? 'up' : 'down'}`} className="ms-1" width="20" height="20" />
</rb.Button>
</div>
<rb.Form onSubmit={onSubmit} validated={validated} noValidate>
{showSettings && (
@ -183,7 +183,7 @@ export default function Receive() {
defaultValue={account}
onChange={(e) => setAccount(parseInt(e.target.value, 10))}
required
disabled={accounts.length === 0}
disabled={isLoading || accounts.length === 0}
>
{accounts.map((val) => (
<option key={val} value={val}>
@ -203,6 +203,7 @@ export default function Receive() {
value={amount}
min={0}
onChange={(e) => setAmount(e.target.value)}
disabled={isLoading}
/>
<rb.Form.Control.Feedback type="invalid">{t('receive.feedback_invalid_amount')}</rb.Form.Control.Feedback>
</rb.Form.Group>
@ -211,7 +212,7 @@ export default function Receive() {
<hr />
<rb.Button variant="outline-dark" type="submit" disabled={isLoading} className="mt-2" style={{ width: '100%' }}>
{isLoading ? (
<div>
<div className="d-flex justify-content-center align-items-center">
<rb.Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" className="me-2" />
{t('receive.text_getting_address')}
</div>

View file

@ -1,6 +1,5 @@
.settings-container {
margin-top: 1.5rem;
height: 2.25rem;
}
.receive-placeholder-container {
@ -20,13 +19,17 @@
}
.receive-placeholder-qr-container {
width: 13.5rem;
margin-bottom: 0;
width: 16.25rem;
}
.receive-placeholder-qr {
width: 13.5rem;
width: 13.75rem;
height: 13.75rem;
margin-top: 1.25rem;
margin-bottom: 1.25rem;
margin: 1.25rem;
}
:root[data-theme='dark'] .receive-placeholder-qr {
width: 16.25rem;
height: 16.25rem;
margin: 0;
}

View file

@ -125,8 +125,5 @@ input::-webkit-inner-spin-button {
.send .input-loader {
height: 3.5rem;
}
.form-group-without-coinjoin-option {
margin-bottom: 3.25rem;
border-radius: 0.25rem;
}

View file

@ -456,7 +456,7 @@ export default function Send() {
<a
href="https://github.com/JoinMarket-Org/joinmarket-clientserver#wallet-features"
target="_blank"
rel="noreferrer"
rel="noopener noreferrer"
>
frozen
</a>
@ -464,7 +464,7 @@ export default function Send() {
<a
href="https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/master/docs/fidelity-bonds.md"
target="_blank"
rel="noreferrer"
rel="noopener noreferrer"
>
time-locked
</a>
@ -475,7 +475,7 @@ export default function Send() {
<a
href="https://github.com/JoinMarket-Org/JoinMarket-Docs/blob/master/High-level-design.md#joinmarket-transaction-types"
target="_blank"
rel="noreferrer"
rel="noopener noreferrer"
>
JoinMarket documentation
</a>
@ -514,31 +514,12 @@ export default function Send() {
)}
<rb.Form onSubmit={onSubmit} noValidate id="send-form">
<rb.Form.Group className="mb-4" controlId="destination">
<rb.Form.Label>{t('send.label_recipient')}</rb.Form.Label>
{isLoading ? (
<rb.Placeholder as="p" animation="wave">
<rb.Placeholder xs={12} className="input-loader" />
</rb.Placeholder>
) : (
<rb.Form.Control
name="destination"
placeholder={t('send.placeholder_recipient')}
className="slashed-zeroes"
value={destination || ''}
required
onChange={(e) => setDestination(e.target.value)}
isInvalid={destination !== null && !isValidAddress(destination)}
/>
)}
<rb.Form.Control.Feedback type="invalid">{t('send.feedback_invalid_recipient')}</rb.Form.Control.Feedback>
</rb.Form.Group>
<rb.Form.Group className="mb-4 flex-grow-1" controlId="account">
<rb.Form.Label>
{settings.useAdvancedWalletMode ? t('send.label_account_dev_mode') : t('send.label_account')}
</rb.Form.Label>
{isLoading ? (
<rb.Placeholder as="p" animation="wave">
<rb.Placeholder as="div" animation="wave">
<rb.Placeholder xs={12} className="input-loader" />
</rb.Placeholder>
) : (
@ -563,14 +544,11 @@ export default function Send() {
</rb.Form.Select>
)}
</rb.Form.Group>
<rb.Form.Group
className={isCoinjoinOptionEnabled ? 'mb-4' : 'form-group-without-coinjoin-option'}
controlId="amount"
>
<rb.Form.Group className={isSweep ? 'mb-0' : 'mb-4'} controlId="amount">
<rb.Form.Label form="send-form">{t('send.label_amount')}</rb.Form.Label>
<div className="position-relative">
{isLoading ? (
<rb.Placeholder as="p" animation="wave">
<rb.Placeholder as="div" animation="wave">
<rb.Placeholder xs={12} className="input-loader" />
</rb.Placeholder>
) : (
@ -609,9 +587,32 @@ export default function Send() {
</rb.Form.Control.Feedback>
{isSweep && frozenOrLockedWarning()}
</rb.Form.Group>
<rb.Form.Group className="mb-4" controlId="destination">
<rb.Form.Label>{t('send.label_recipient')}</rb.Form.Label>
{isLoading ? (
<rb.Placeholder as="div" animation="wave">
<rb.Placeholder xs={12} className="input-loader" />
</rb.Placeholder>
) : (
<rb.Form.Control
name="destination"
placeholder={t('send.placeholder_recipient')}
className="slashed-zeroes"
value={destination || ''}
required
onChange={(e) => setDestination(e.target.value)}
isInvalid={destination !== null && !isValidAddress(destination)}
/>
)}
<rb.Form.Control.Feedback type="invalid">{t('send.feedback_invalid_recipient')}</rb.Form.Control.Feedback>
</rb.Form.Group>
{isCoinjoinOptionEnabled && (
<rb.Form.Group controlId="isCoinjoin" className={`${isCoinjoin ? 'mb-3' : ''}`}>
<ToggleSwitch label={t('send.toggle_coinjoin')} onToggle={(isToggled) => setIsCoinjoin(isToggled)} />
<ToggleSwitch
label={t('send.toggle_coinjoin')}
initialValue={isCoinjoin}
onToggle={(isToggled) => setIsCoinjoin(isToggled)}
/>
</rb.Form.Group>
)}
</rb.Form>
@ -624,7 +625,7 @@ export default function Send() {
)}
<rb.Button variant="dark" type="submit" disabled={isSending || !formIsValid} className="mt-4" form="send-form">
{isSending ? (
<div>
<div className="d-flex justify-content-center align-items-center">
<rb.Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" className="me-2" />
{t('send.text_sending')}
</div>