Update TopUpModal.tsx

This commit is contained in:
Egor
2026-01-19 01:10:03 +03:00
committed by GitHub
parent 3e8f547b92
commit daefd5cdb4

View File

@@ -41,7 +41,6 @@ interface TopUpModalProps {
export default function TopUpModal({ method, onClose, initialAmountRubles }: TopUpModalProps) { export default function TopUpModal({ method, onClose, initialAmountRubles }: TopUpModalProps) {
const { t } = useTranslation() const { t } = useTranslation()
const { formatAmount, currencySymbol, convertAmount, convertToRub, targetCurrency } = useCurrency() const { formatAmount, currencySymbol, convertAmount, convertToRub, targetCurrency } = useCurrency()
const containerRef = useRef<HTMLDivElement>(null)
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const getInitialAmount = (): string => { const getInitialAmount = (): string => {
@@ -58,7 +57,7 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
const [selectedOption, setSelectedOption] = useState<string | null>( const [selectedOption, setSelectedOption] = useState<string | null>(
method.options && method.options.length > 0 ? method.options[0].id : null method.options && method.options.length > 0 ? method.options[0].id : null
) )
const [isKeyboardOpen, setIsKeyboardOpen] = useState(false) const [isInputFocused, setIsInputFocused] = useState(false)
const popupRef = useRef<Window | null>(null) const popupRef = useRef<Window | null>(null)
const hasOptions = method.options && method.options.length > 0 const hasOptions = method.options && method.options.length > 0
@@ -69,31 +68,6 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
const methodName = t(`balance.paymentMethods.${methodKey}.name`, { defaultValue: '' }) || method.name const methodName = t(`balance.paymentMethods.${methodKey}.name`, { defaultValue: '' }) || method.name
const isTelegramMiniApp = typeof window !== 'undefined' && Boolean(window.Telegram?.WebApp?.initData) const isTelegramMiniApp = typeof window !== 'undefined' && Boolean(window.Telegram?.WebApp?.initData)
// Detect keyboard open/close
useEffect(() => {
const handleFocus = () => setIsKeyboardOpen(true)
const handleBlur = () => setTimeout(() => setIsKeyboardOpen(false), 100)
const input = inputRef.current
if (input) {
input.addEventListener('focus', handleFocus)
input.addEventListener('blur', handleBlur)
return () => {
input.removeEventListener('focus', handleFocus)
input.removeEventListener('blur', handleBlur)
}
}
}, [])
// Scroll to input when keyboard opens
useEffect(() => {
if (isKeyboardOpen && containerRef.current && inputRef.current) {
setTimeout(() => {
inputRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}, 150)
}
}, [isKeyboardOpen])
const starsPaymentMutation = useMutation({ const starsPaymentMutation = useMutation({
mutationFn: (amountKopeks: number) => balanceApi.createStarsInvoice(amountKopeks), mutationFn: (amountKopeks: number) => balanceApi.createStarsInvoice(amountKopeks),
onSuccess: (data) => { onSuccess: (data) => {
@@ -172,26 +146,24 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
const inputStep = currencyDecimals === 0 ? 1 : 0.01 const inputStep = currencyDecimals === 0 ? 1 : 0.01
const isPending = topUpMutation.isPending || starsPaymentMutation.isPending const isPending = topUpMutation.isPending || starsPaymentMutation.isPending
return ( // When input is focused in Telegram Mini App, position modal at top
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-end sm:items-center justify-center"> const modalPosition = isInputFocused && isTelegramMiniApp ? 'items-start pt-2' : 'items-end sm:items-center'
<div className="absolute inset-0" onClick={onClose} />
<div return (
ref={containerRef} <div className={`fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex justify-center ${modalPosition}`}>
className="relative w-full sm:max-w-md sm:mx-4 bg-dark-900 sm:rounded-2xl rounded-t-2xl border border-dark-700/50 shadow-2xl animate-slide-up max-h-[85vh] overflow-hidden flex flex-col" <div className="absolute inset-0" onClick={() => { inputRef.current?.blur(); onClose() }} />
>
{/* Compact Header */} <div className="relative w-full sm:max-w-md sm:mx-4 bg-dark-900 sm:rounded-2xl rounded-t-2xl border border-dark-700/50 shadow-2xl animate-slide-up">
<div className="flex-shrink-0 flex justify-between items-center px-4 py-3 border-b border-dark-800"> {/* Header - hide when keyboard open on mobile */}
<div className={`flex justify-between items-center px-4 py-3 border-b border-dark-800 ${isInputFocused && isTelegramMiniApp ? 'hidden' : ''}`}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-accent-500/20 flex items-center justify-center"> <div className="w-8 h-8 rounded-lg bg-accent-500/20 flex items-center justify-center">
<svg className="w-4 h-4 text-accent-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <svg className="w-4 h-4 text-accent-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> <path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg> </svg>
</div> </div>
<div>
<h2 className="text-base font-semibold text-dark-100">{methodName}</h2> <h2 className="text-base font-semibold text-dark-100">{methodName}</h2>
</div> </div>
</div>
<button onClick={onClose} className="w-8 h-8 rounded-lg bg-dark-800 hover:bg-dark-700 flex items-center justify-center"> <button onClick={onClose} className="w-8 h-8 rounded-lg bg-dark-800 hover:bg-dark-700 flex items-center justify-center">
<svg className="w-4 h-4 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <svg className="w-4 h-4 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /> <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
@@ -199,11 +171,10 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
</button> </button>
</div> </div>
{/* Scrollable Content */} {/* Content */}
<div className="flex-1 overflow-y-auto overscroll-contain">
<div className="p-4 space-y-3"> <div className="p-4 space-y-3">
{/* Payment options - compact grid */} {/* Payment options - hide when keyboard open */}
{hasOptions && method.options && ( {hasOptions && method.options && !isInputFocused && (
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
{method.options.map((option) => ( {method.options.map((option) => (
<button <button
@@ -222,7 +193,7 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
</div> </div>
)} )}
{/* Amount input with currency */} {/* Amount input */}
<div className="relative"> <div className="relative">
<input <input
ref={inputRef} ref={inputRef}
@@ -230,6 +201,8 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
inputMode="decimal" inputMode="decimal"
value={amount} value={amount}
onChange={(e) => setAmount(e.target.value)} onChange={(e) => setAmount(e.target.value)}
onFocus={() => setIsInputFocused(true)}
onBlur={() => setTimeout(() => setIsInputFocused(false), 150)}
placeholder={`${formatAmount(minRubles, 0)} ${formatAmount(maxRubles, 0)}`} placeholder={`${formatAmount(minRubles, 0)} ${formatAmount(maxRubles, 0)}`}
step={inputStep} step={inputStep}
className="w-full h-14 px-4 pr-16 text-xl font-semibold bg-dark-800 border border-dark-700 rounded-xl text-dark-100 placeholder:text-dark-500 focus:outline-none focus:border-accent-500 focus:ring-1 focus:ring-accent-500" className="w-full h-14 px-4 pr-16 text-xl font-semibold bg-dark-800 border border-dark-700 rounded-xl text-dark-100 placeholder:text-dark-500 focus:outline-none focus:border-accent-500 focus:ring-1 focus:ring-accent-500"
@@ -240,7 +213,7 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
</span> </span>
</div> </div>
{/* Quick amounts - compact */} {/* Quick amounts - always show, they help dismiss keyboard */}
{quickAmounts.length > 0 && ( {quickAmounts.length > 0 && (
<div className="grid grid-cols-4 gap-2"> <div className="grid grid-cols-4 gap-2">
{quickAmounts.map((a) => { {quickAmounts.map((a) => {
@@ -267,22 +240,19 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
</div> </div>
)} )}
{/* Error */} {/* Error - hide when keyboard open */}
{error && ( {error && !isInputFocused && (
<div className="bg-error-500/10 border border-error-500/30 text-error-400 px-3 py-2 rounded-xl text-sm"> <div className="bg-error-500/10 border border-error-500/30 text-error-400 px-3 py-2 rounded-xl text-sm">
{error} {error}
</div> </div>
)} )}
</div>
</div>
{/* Fixed Footer with Submit */} {/* Submit button - always visible */}
<div className="flex-shrink-0 p-4 pt-3 border-t border-dark-800 bg-dark-900 safe-area-inset-bottom">
<button <button
type="button" type="button"
onClick={handleSubmit} onClick={handleSubmit}
disabled={isPending || !amount} disabled={isPending || !amount}
className="btn-primary w-full h-12 text-base font-semibold" className="btn-primary w-full h-12 text-base font-semibold mt-2"
> >
{isPending ? ( {isPending ? (
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" /> <span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
@@ -295,14 +265,21 @@ export default function TopUpModal({ method, onClose, initialAmountRubles }: Top
</span> </span>
)} )}
</button> </button>
{/* Cancel - hide when keyboard open */}
{!isInputFocused && (
<button <button
type="button" type="button"
onClick={onClose} onClick={onClose}
className="w-full mt-2 py-2 text-sm text-dark-400 hover:text-dark-200" className="w-full py-2 text-sm text-dark-400 hover:text-dark-200"
> >
{t('common.cancel')} {t('common.cancel')}
</button> </button>
)}
</div> </div>
{/* Safe area for iPhone */}
<div className="safe-area-inset-bottom" />
</div> </div>
</div> </div>
) )