Repository: joinmarket-webui/jam (ts)

Issue:
UI: Remove second B Sign when frozen
it would be awesome to make the landing page cleaner without second B for frozen Value, please see screenshot. 
Same width for upper B and Frozen Icon so that everything is tidy and clean 

![Bildschirmfoto 2023-11-21 um 10 46 48](https://github.com/joinmarket-webui/jam/assets/96262473/0fe9b60b-d86a-45b8-912c-f9b800692237)


Pull request:
ui(Jar): hide currency symbol of frozen balance
Resolves #694.

Hides the currency symbol for frozen balances in component `Jar`.
Also, available and frozen balance use the same font size.

<img src="https://github.com/joinmarket-webui/jam/assets/3358649/62cd7183-4528-4362-b530-1c07f6fb7af2" width=350 /><br /><img src="https://github.com/joinmarket-webui/jam/assets/3358649/18857637-a869-4d04-8705-23b972532343" width=350 /><br /><img src="https://github.com/joinmarket-webui/jam/assets/3358649/ba23aa0a-e3b1-4709-9703-aa0895744732" width=350 />




Patch:
diff --git a/src/components/Balance.module.css b/src/components/Balance.module.css
index ced0b16..013532e 100644
--- a/src/components/Balance.module.css
+++ b/src/components/Balance.module.css
@@ -22,23 +22,35 @@
   color: var(--jam-balance-color);
 }
 
-.frozenSymbol {
-  order: -2;
+.hideSymbol {
+  padding-left: 0.1em;
+  color: var(--jam-balance-deemphasize-color);
 }
 
 .bitcoinSymbol {
-  padding-right: 0.1em;
   order: -1;
+  width: 1em;
+  padding-right: 0.1em;
 }
 
 .satsSymbol {
-  padding-right: 0.1em;
-  order: 6;
+  order: 5;
 }
 
-.hideSymbol {
-  padding-left: 0.1em;
-  color: var(--jam-balance-deemphasize-color);
+.frozenSymbol {
+  order: 5;
+}
+.bitcoinAmount + .frozenSymbol {
+  order: -2;
+  width: 1em;
+  height: 1em;
+}
+
+.frozenSymbol,
+.bitcoinSymbol,
+.satsSymbol {
+  display: flex;
+  justify-content: center;
 }
 
 .bitcoinAmount .fractionalPart :nth-child(3)::before,
diff --git a/src/components/Balance.tsx b/src/components/Balance.tsx
index 9d49437..880d0f6 100644
--- a/src/components/Balance.tsx
+++ b/src/components/Balance.tsx
@@ -1,4 +1,4 @@
-import { MouseEventHandler, useCallback, useEffect, useMemo, useState } from 'react'
+import { PropsWithChildren, MouseEventHandler, useEffect, useMemo, useState } from 'react'
 import classNames from 'classnames'
 import Sprite from './Sprite'
 import { SATS, BTC, btcToSats, satsToBtc, isValidNumber, formatBtc, formatSats } from '../utils'
@@ -15,74 +15,99 @@ const getDisplayMode = (unit: Unit, showBalance: boolean) => {
   return DISPLAY_MODE_HIDDEN
 }
 
-const DECIMAL_POINT_CHAR = '.'
-
-const BitcoinAmountComponent = ({ value }: { value: number }) => {
-  const numberString = formatBtc(value)
-  const [integerPart, fractionalPart] = numberString.split(DECIMAL_POINT_CHAR)
-
-  const fractionPartArray = fractionalPart.split('')
-  const integerPartIsZero = integerPart === '0'
-  const fractionalPartStartsWithZero = fractionPartArray[0] === '0'
-
-  return (
-    <span
-      className={`${styles.bitcoinAmount} slashed-zeroes`}
-      data-testid="bitcoin-amount"
-      data-integer-part-is-zero={integerPartIsZero}
-      data-fractional-part-starts-with-zero={fractionalPartStartsWithZero}
-      data-raw-value={value}
-      data-formatted-value={numberString}
-    >
-      <span className={styles.integerPart}>{integerPart}</span>
-      <span className={styles.decimalPoint}>{DECIMAL_POINT_CHAR}</span>
-      <span className={styles.fractionalPart}>
-        {fractionPartArray.map((digit, index) => (
-          <span key={index} data-digit={digit}>
-            {digit}
-          </span>
-        ))}
-      </span>
-    </span>
-  )
-}
-
-const SatsAmountComponent = ({ value }: { value: number }) => {
-  return (
-    <span className={`${styles.satsAmount} slashed-zeroes`} data-testid="sats-amount" data-raw-value={value}>
-      {formatSats(value)}
-    </span>
-  )
-}
-
-const BTC_SYMBOL = <span className={styles.bitcoinSymbol}>{'\u20BF'}</span>
+const BTC_SYMBOL = (
+  <span data-testid="bitcoin-symbol" className={styles.bitcoinSymbol}>
+    {'\u20BF'}
+  </span>
+)
 
-const SAT_SYMBOL = <Sprite className={styles.satsSymbol} symbol="sats" width="1.2em" height="1.2em" />
+const SAT_SYMBOL = (
+  <Sprite data-testid="sats-symbol" className={styles.satsSymbol} symbol="sats" width="1.2em" height="1.2em" />
+)
 
 const FROZEN_SYMBOL = (
-  <Sprite className={`${styles.frozenSymbol} frozen-symbol-hook`} symbol="snowflake" width="1.2em" height="1.2em" />
+  <Sprite
+    data-testid="frozen-symbol"
+    className={`${styles.frozenSymbol} frozen-symbol-hook`}
+    symbol="snowflake"
+    width="1.2em"
+    height="1.2em"
+  />
 )
 
 interface BalanceComponentProps {
-  symbol: JSX.Element
-  value: JSX.Element
+  symbol?: JSX.Element
+  showSymbol?: boolean
   frozen?: boolean
 }
 
-const BalanceComponent = ({ symbol, value, frozen = false }: BalanceComponentProps) => {
+const BalanceComponent = ({
+  symbol,
+  showSymbol = true,
+  frozen = false,
+  children,
+}: PropsWithChildren<BalanceComponentProps>) => {
   return (
     <span
       className={classNames(styles.balance, 'balance-hook', 'd-inline-flex align-items-center', {
         [styles.frozen]: frozen,
       })}
     >
+      {children}
+      {showSymbol && symbol}
       {frozen && FROZEN_SYMBOL}
-      {value}
-      {symbol}
     </span>
   )
 }
 
+const DECIMAL_POINT_CHAR = '.'
+
+type BitcoinBalanceProps = Omit<BalanceComponentProps, 'symbol'> & { value: number }
+
+const BitcoinBalance = ({ value, ...props }: BitcoinBalanceProps) => {
+  const numberString = formatBtc(value)
+  const [integerPart, fractionalPart] = numberString.split(DECIMAL_POINT_CHAR)
+
+  const fractionPartArray = fractionalPart.split('')
+  const integerPartIsZero = integerPart === '0'
+  const fractionalPartStartsWithZero = fractionPartArray[0] === '0'
+
+  return (
+    <BalanceComponent symbol={BTC_SYMBOL} {...props}>
+      <span
+        className={`${styles.bitcoinAmount} slashed-zeroes`}
+        data-testid="bitcoin-amount"
+        data-integer-part-is-zero={integerPartIsZero}
+        data-fractional-part-starts-with-zero={fractionalPartStartsWithZero}
+        data-raw-value={value}
+        data-formatted-value={numberString}
+      >
+        <span className={styles.integerPart}>{integerPart}</span>
+        <span className={styles.decimalPoint}>{DECIMAL_POINT_CHAR}</span>
+        <span className={styles.fractionalPart}>
+          {fractionPartArray.map((digit, index) => (
+            <span key={index} data-digit={digit}>
+              {digit}
+            </span>
+          ))}
+        </span>
+      </span>
+    </BalanceComponent>
+  )
+}
+
+type SatsBalanceProps = Omit<BalanceComponentProps, 'symbol'> & { value: number }
+
+const SatsBalance = ({ value, ...props }: SatsBalanceProps) => {
+  return (
+    <BalanceComponent symbol={SAT_SYMBOL} {...props}>
+      <span className={`${styles.satsAmount} slashed-zeroes`} data-testid="sats-amount" data-raw-value={value}>
+        {formatSats(value)}
+      </span>
+    </BalanceComponent>
+  )
+}
+
 /**
  * Options argument for Balance component.
  *
@@ -97,12 +122,11 @@ const BalanceComponent = ({ symbol, value, frozen = false }: BalanceComponentPro
  * @param {loading}: A loading flag that renders a placeholder while true.
  * @param {enableVisibilityToggle}: A flag that controls whether the balance can mask/unmask when clicked
  */
-interface BalanceProps {
+type BalanceProps = Omit<BalanceComponentProps, 'symbol'> & {
   valueString: string
   convertToUnit: Unit
   showBalance?: boolean
   enableVisibilityToggle?: boolean
-  frozen?: boolean
 }
 
 /**
@@ -113,7 +137,7 @@ export default function Balance({
   convertToUnit,
   showBalance = false,
   enableVisibilityToggle = !showBalance,
-  frozen = false,
+  ...props
 }: BalanceProps) {
   const [isBalanceVisible, setIsBalanceVisible] = useState(showBalance)
   const displayMode = useMemo(() => getDisplayMode(convertToUnit, isBalanceVisible), [convertToUnit, isBalanceVisible])
@@ -122,28 +146,29 @@ export default function Balance({
     setIsBalanceVisible(showBalance)
   }, [showBalance])
 
-  const toggleVisibility: MouseEventHandler = useCallback((e) => {
+  const toggleVisibility: MouseEventHandler = (e) => {
     e.preventDefault()
     e.stopPropagation()
 
     setIsBalanceVisible((current) => !current)
-  }, [])
+  }
 
   const balanceComponent = useMemo(() => {
     if (displayMode === DISPLAY_MODE_HIDDEN) {
       return (
         <BalanceComponent
           symbol={<Sprite symbol="hide" width="1.2em" height="1.2em" className={styles.hideSymbol} />}
-          value={<span className="slashed-zeroes">{'*****'}</span>}
-          frozen={frozen}
-        />
+          {...props}
+        >
+          <span className="slashed-zeroes">{'*****'}</span>
+        </BalanceComponent>
       )
     }
 
     const valueNumber = parseFloat(valueString)
     if (!isValidNumber(valueNumber)) {
       console.warn('<Balance /> component expects number input as string')
-      return <BalanceComponent symbol={<></>} value={<>{valueString}</>} frozen={frozen} />
+      return <BalanceComponent {...props}>{valueString}</BalanceComponent>
     }
 
     // Treat integers as sats.
@@ -151,35 +176,17 @@ export default function Balance({
     // Treat decimal numbers as btc.
     const valueIsBtc = !valueIsSats && valueString.indexOf('.') > -1
 
-    if (valueIsBtc && displayMode === DISPLAY_MODE_BTC)
-      return (
-        <BalanceComponent symbol={BTC_SYMBOL} value={<BitcoinAmountComponent value={valueNumber} />} frozen={frozen} />
-      )
-    if (valueIsSats && displayMode === DISPLAY_MODE_SATS)
-      return (
-        <BalanceComponent symbol={SAT_SYMBOL} value={<SatsAmountComponent value={valueNumber} />} frozen={frozen} />
-      )
+    if (valueIsBtc && displayMode === DISPLAY_MODE_BTC) return <BitcoinBalance value={valueNumber} {...props} />
+    if (valueIsSats && displayMode === DISPLAY_MODE_SATS) return <SatsBalance value={valueNumber} {...props} />
 
     if (valueIsBtc && displayMode === DISPLAY_MODE_SATS)
-      return (
-        <BalanceComponent
-          symbol={SAT_SYMBOL}
-          value={<SatsAmountComponent value={btcToSats(valueString)} />}
-          frozen={frozen}
-        />
-      )
+      return <SatsBalance value={btcToSats(valueString)} {...props} />
     if (valueIsSats && displayMode === DISPLAY_MODE_BTC)
-      return (
-        <BalanceComponent
-          symbol={BTC_SYMBOL}
-          value={<BitcoinAmountComponent value={satsToBtc(valueString)} />}
-          frozen={frozen}
-        />
-      )
+      return <BitcoinBalance value={satsToBtc(valueString)} {...props} />
 
     console.warn('<Balance /> component cannot determine balance format')
-    return <BalanceComponent symbol={<></>} value={<>{valueString}</>} frozen={frozen} />
-  }, [valueString, displayMode, frozen])
+    return <BalanceComponent {...props}>{valueString}</BalanceComponent>
+  }, [valueString, displayMode, props])
 
   if (!enableVisibilityToggle) {
     return <>{balanceComponent}</>
diff --git a/src/components/jars/Jar.module.css b/src/components/jars/Jar.module.css
index b24d8b0..9cb838d 100644
--- a/src/components/jars/Jar.module.css
+++ b/src/components/jars/Jar.module.css
@@ -58,10 +58,6 @@
   font-size: 0.8rem;
 }
 
-.frozen.jarBalance {
-  font-size: 0.7rem;
-}
-
 .selectableJarContainer {
   display: flex;
   flex-direction: column;
diff --git a/src/components/jars/Jar.tsx b/src/components/jars/Jar.tsx
index 4def150..5b2fae6 100644
--- a/src/components/jars/Jar.tsx
+++ b/src/components/jars/Jar.tsx
@@ -135,6 +135,7 @@ const Jar = ({ index, balance, frozenBalance, fillLevel, isOpen = false }: JarPr
               convertToUnit={settings.unit}
               showBalance={settings.showBalance}
               frozen={true}
+              showSymbol={false}
             />
           )}
         </div>


Test patch:
diff --git a/src/components/Balance.test.tsx b/src/components/Balance.test.tsx
index 9429387..68377b1 100644
--- a/src/components/Balance.test.tsx
+++ b/src/components/Balance.test.tsx
@@ -11,31 +11,46 @@ describe('<Balance />', () => {
     expect(screen.getByText(`NaN`)).toBeInTheDocument()
   })
 
-  it('should render BTC using satscomma formatting', () => {
+  it('should render balance in BTC', () => {
     render(<Balance valueString={'123.456'} convertToUnit={BTC} showBalance={true} />)
     expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`123.45600000`)
+    expect(screen.getByTestId('bitcoin-symbol')).toBeVisible()
+    expect(screen.queryByTestId('sats-symbol')).not.toBeInTheDocument()
+    expect(screen.queryByTestId('frozen-symbol')).not.toBeInTheDocument()
+  })
+
+  it('should render balance in SATS', () => {
+    render(<Balance valueString={'123.456'} convertToUnit={SATS} showBalance={true} />)
+    expect(screen.getByTestId('sats-amount')).toHaveTextContent(`12,345,600,000`)
+    expect(screen.getByTestId('sats-symbol')).toBeVisible()
+    expect(screen.queryByTestId('bitcoin-symbol')).not.toBeInTheDocument()
+    expect(screen.queryByTestId('frozen-symbol')).not.toBeInTheDocument()
   })
 
   it('should hide balance for BTC by default', () => {
     render(<Balance valueString={'123.456'} convertToUnit={BTC} />)
     expect(screen.getByText(`*****`)).toBeInTheDocument()
     expect(screen.queryByTestId('bitcoin-amount')).not.toBeInTheDocument()
+    expect(screen.queryByTestId('bitcoin-symbol')).not.toBeInTheDocument()
   })
 
   it('should hide balance for SATS by default', () => {
     render(<Balance valueString={'123'} convertToUnit={SATS} />)
     expect(screen.getByText(`*****`)).toBeInTheDocument()
     expect(screen.queryByTestId(`sats-amount`)).not.toBeInTheDocument()
+    expect(screen.queryByTestId('sats-symbol')).not.toBeInTheDocument()
   })
 
   it('should render a string BTC value correctly as BTC', () => {
     render(<Balance valueString={'123.03224961'} convertToUnit={BTC} showBalance={true} />)
     expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`123.03224961`)
+    expect(screen.getByTestId('bitcoin-symbol')).toBeVisible()
   })
 
   it('should render a string BTC value correctly as SATS', () => {
     render(<Balance valueString={'123.03224961'} convertToUnit={SATS} showBalance={true} />)
     expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`12,303,224,961`)
+    expect(screen.getByTestId('sats-symbol')).toBeVisible()
   })
 
   it('should render a zero string BTC value correctly as BTC', () => {
@@ -108,6 +123,27 @@ describe('<Balance />', () => {
     expect(screen.getByTestId(`sats-amount`)).toHaveTextContent(`2,100,000,000,000,000`)
   })
 
+  it('should render frozen balance in BTC', () => {
+    render(<Balance valueString={'123.456'} convertToUnit={BTC} showBalance={true} frozen={true} />)
+    expect(screen.getByTestId('bitcoin-amount').dataset.formattedValue).toBe(`123.45600000`)
+    expect(screen.getByTestId('bitcoin-symbol')).toBeVisible()
+    expect(screen.getByTestId('frozen-symbol')).toBeVisible()
+  })
+
+  it('should render frozen balance in SATS', () => {
+    render(<Balance valueString={'123.456'} convertToUnit={SATS} showBalance={true} frozen={true} />)
+    expect(screen.getByTestId('sats-amount')).toHaveTextContent(`12,345,600,000`)
+    expect(screen.getByTestId('sats-symbol')).toBeVisible()
+    expect(screen.getByTestId('frozen-symbol')).toBeVisible()
+  })
+
+  it('should render balance without symbol', () => {
+    render(<Balance valueString={'123.456'} convertToUnit={SATS} showBalance={true} frozen={true} showSymbol={false} />)
+    expect(screen.getByTestId('sats-amount')).toBeVisible()
+    expect(screen.getByTestId('frozen-symbol')).toBeVisible()
+    expect(screen.queryByTestId('sats-symbol')).not.toBeInTheDocument()
+  })
+
   it('should toggle visibility of initially hidden balance on click by default', () => {
     render(<Balance valueString={`21`} convertToUnit={SATS} showBalance={false} />)
     expect(screen.queryByTestId(`sats-amount`)).not.toBeInTheDocument()