diff --git a/components/modals/BalanceInfo.tsx b/components/modals/BalanceInfo.tsx index 7d463466c..b680a1c7d 100644 --- a/components/modals/BalanceInfo.tsx +++ b/components/modals/BalanceInfo.tsx @@ -1,6 +1,9 @@ 'use client'; import { useUser } from '@/contexts/UserContext'; +import { useCurrencyPreference } from '@/contexts/CurrencyPreferenceContext'; +import { useExchangeRate } from '@/contexts/ExchangeRateContext'; +import { formatCurrency } from '@/utils/currency'; interface BalanceInfoProps { amount: number; @@ -14,19 +17,43 @@ export function BalanceInfo({ includeLockedBalance = false, }: BalanceInfoProps) { const { user } = useUser(); + const { showUSD } = useCurrencyPreference(); + const { exchangeRate } = useExchangeRate(); const userBalance = user?.balance || 0; const lockedBalance = user?.lockedBalance || 0; const totalAvailableBalance = includeLockedBalance ? userBalance + lockedBalance : userBalance; + const useUsd = showUSD && exchangeRate > 0; + const primary = formatCurrency({ amount: totalAvailableBalance, showUSD: useUsd, exchangeRate }); + const secondary = + exchangeRate > 0 + ? formatCurrency({ amount: totalAvailableBalance, showUSD: !useUsd, exchangeRate }) + : undefined; + + const deficit = amount - totalAvailableBalance; + const deficitFormatted = + deficit > 0 ? formatCurrency({ amount: deficit, showUSD: useUsd, exchangeRate }) : undefined; + + const primaryUnit = useUsd ? 'USD' : 'RSC'; + const primaryWithUnit = `${primary} ${primaryUnit}`; + + const secondaryUnit = useUsd ? 'RSC' : 'USD'; + const secondaryWithUnit = secondary ? `${secondary} ${secondaryUnit}` : undefined; + + const deficitWithUnit = deficitFormatted ? `${deficitFormatted} ${primaryUnit}` : undefined; + return (
Current RSC Balance: - {totalAvailableBalance.toLocaleString()} RSC + {primaryWithUnit}
- {showWarning && ( + {secondaryWithUnit && ( +
≈ {secondaryWithUnit}
+ )} + {showWarning && deficitWithUnit && (
- {`You need ${(amount - totalAvailableBalance).toLocaleString()} RSC more for this contribution`} + {`You need ${deficitWithUnit} more for this contribution`}
)}
diff --git a/components/modals/ContributeToFundraiseModal.tsx b/components/modals/ContributeToFundraiseModal.tsx index fe8f0c8d2..2d74045cf 100644 --- a/components/modals/ContributeToFundraiseModal.tsx +++ b/components/modals/ContributeToFundraiseModal.tsx @@ -1,7 +1,7 @@ 'use client'; import { Dialog, Transition } from '@headlessui/react'; -import { Fragment, useState } from 'react'; +import { Fragment, useEffect, useState } from 'react'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/form/Input'; import { Alert } from '@/components/ui/Alert'; @@ -12,6 +12,8 @@ import { toast } from 'react-hot-toast'; import { FundraiseService } from '@/services/fundraise.service'; import { useUser } from '@/contexts/UserContext'; import { useExchangeRate } from '@/contexts/ExchangeRateContext'; +import { useCurrencyPreference } from '@/contexts/CurrencyPreferenceContext'; +import { formatCurrency } from '@/utils/currency'; import { Fundraise } from '@/types/funding'; interface ContributeToFundraiseModalProps { @@ -21,15 +23,18 @@ interface ContributeToFundraiseModalProps { fundraise: Fundraise; } -// Currency Input Component const CurrencyInput = ({ value, onChange, error, + currencyLabel, + helperText, }: { value: string; onChange: (e: React.ChangeEvent) => void; error?: string; + currencyLabel: string; + helperText?: string; }) => { return (
@@ -45,62 +50,74 @@ const CurrencyInput = ({ className={`w-full text-left h-12 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 ${error ? 'border-red-500' : ''}`} rightElement={
- RSC + {currencyLabel}
} + helperText={helperText} /> {error &&

{error}

}
); }; -// Fee Breakdown Component const FeeBreakdown = ({ contributionAmount, platformFee, totalAmount, + showUSD, + exchangeRate, }: { contributionAmount: number; platformFee: number; totalAmount: number; -}) => ( -
-
- Your contribution: - {contributionAmount.toLocaleString()} RSC -
+ showUSD: boolean; + exchangeRate: number; +}) => { + const useUsd = showUSD && exchangeRate > 0; + const formatAmt = (v: number) => { + const s = formatCurrency({ amount: v, showUSD: useUsd, exchangeRate }); + return useUsd ? `${s} USD` : `${s} RSC`; + }; + + return ( +
+
+ Your contribution: + {formatAmt(contributionAmount)} +
-
-
-
- Platform fees (9%) - -
- - - -
-
+
+
+
+ Platform fees (9%) + +
+ + + +
+
+
+ + {formatAmt(platformFee)}
- + {platformFee.toLocaleString()} RSC
-
-
+
-
- Total amount: - {totalAmount.toLocaleString()} RSC +
+ Total amount: + {formatAmt(totalAmount)} +
-
-); + ); +}; const ModalHeader = ({ title, @@ -140,68 +157,101 @@ export function ContributeToFundraiseModal({ fundraise, }: ContributeToFundraiseModalProps) { const { user } = useUser(); - const [inputAmount, setInputAmount] = useState(100); + const { showUSD } = useCurrencyPreference(); + const { exchangeRate } = useExchangeRate(); + const [inputAmountRsc, setInputAmountRsc] = useState(100); const [isContributing, setIsContributing] = useState(false); const [error, setError] = useState(null); const [isSuccess, setIsSuccess] = useState(false); const [amountError, setAmountError] = useState(undefined); - // Calculate total available balance including locked balance for fundraise contributions const userBalance = user?.balance || 0; const lockedBalance = user?.lockedBalance || 0; const totalAvailableBalance = userBalance + lockedBalance; - // Utility functions + const useUsd = showUSD && exchangeRate > 0; + const handleAmountChange = (e: React.ChangeEvent) => { const rawValue = e.target.value.replace(/[^0-9.]/g, ''); const numValue = parseFloat(rawValue); - if (!isNaN(numValue)) { - setInputAmount(numValue); + if (Number.isNaN(numValue)) { + setInputAmountRsc(0); + setAmountError('Please enter a valid amount'); + return; + } + + // Convert from preferred currency to RSC + const rsc = useUsd ? numValue / exchangeRate : numValue; + setInputAmountRsc(rsc); - // Validate minimum amount - if (numValue < 10) { - setAmountError('Minimum contribution amount is 10 RSC'); - } else { - setAmountError(undefined); - } + // Validate minimum amount (10 RSC) + if (rsc < 10) { + const minUsd = 10 * exchangeRate; + setAmountError( + useUsd + ? `Minimum contribution amount is $${minUsd.toFixed(2)} USD` + : 'Minimum contribution amount is 10 RSC' + ); } else { - setInputAmount(0); - setAmountError('Please enter a valid amount'); + setAmountError(undefined); } }; + // Keep min-amount validation message in sync with currency preference/exchange rate + useEffect(() => { + if (inputAmountRsc > 0 && inputAmountRsc < 10) { + const minUsd = 10 * exchangeRate; + setAmountError( + useUsd + ? `Minimum contribution amount is $${minUsd.toFixed(2)} USD` + : 'Minimum contribution amount is 10 RSC' + ); + } + }, [useUsd, exchangeRate, inputAmountRsc]); + const getFormattedInputValue = () => { - if (inputAmount === 0) return ''; - return inputAmount.toLocaleString(); + if (inputAmountRsc === 0) return ''; + const displayValueNum = useUsd ? inputAmountRsc * exchangeRate : inputAmountRsc; + return displayValueNum.toLocaleString(undefined, { maximumFractionDigits: 2 }); }; + let secondaryText: string | undefined; + if (exchangeRate > 0) { + const otherFormatted = formatCurrency({ + amount: inputAmountRsc, + showUSD: !useUsd, + exchangeRate, + }); + const secondaryUnit = useUsd ? 'RSC' : 'USD'; + secondaryText = `≈ ${otherFormatted} ${secondaryUnit}`; + } + const handleContribute = async () => { try { - // Validate minimum amount before proceeding - if (inputAmount < 10) { - setError('Minimum contribution amount is 10 RSC'); + if (inputAmountRsc < 10) { + const minUsd = 10 * exchangeRate; + setError( + useUsd + ? `Minimum contribution amount is $${minUsd.toFixed(2)} USD` + : 'Minimum contribution amount is 10 RSC' + ); return; } setIsContributing(true); setError(null); - // Pass the contribution amount without the platform fee - // The API expects the net contribution amount - await FundraiseService.contributeToFundraise(fundraise.id, inputAmount); + await FundraiseService.contributeToFundraise(fundraise.id, inputAmountRsc); toast.success('Your contribution has been successfully added to the fundraise.'); - // Set success flag setIsSuccess(true); - // Call onContributeSuccess if provided if (onContributeSuccess) { onContributeSuccess(); } - // Close the modal onClose(); } catch (error) { console.error('Failed to contribute to fundraise:', error); @@ -211,8 +261,8 @@ export function ContributeToFundraiseModal({ } }; - const platformFee = Math.round(inputAmount * 0.09 * 100) / 100; - const totalAmount = inputAmount + platformFee; + const platformFee = Math.round(inputAmountRsc * 0.09 * 100) / 100; + const totalAmount = inputAmountRsc + platformFee; const insufficientBalance = totalAvailableBalance < totalAmount; return ( @@ -266,6 +316,8 @@ export function ContributeToFundraiseModal({ value={getFormattedInputValue()} onChange={handleAmountChange} error={amountError} + currencyLabel={useUsd ? 'USD' : 'RSC'} + helperText={secondaryText} />
@@ -275,9 +327,11 @@ export function ContributeToFundraiseModal({

Fees Breakdown

@@ -299,10 +353,10 @@ export function ContributeToFundraiseModal({ variant="default" disabled={ isContributing || - !inputAmount || + !inputAmountRsc || insufficientBalance || !!amountError || - inputAmount < 10 + inputAmountRsc < 10 } className="w-full h-12 text-base" onClick={handleContribute}